Java-redis分布式鎖 搶購秒殺系統 實現


一、使用分布式鎖要滿足的幾個條件

  1. 系統是一個分布式系統(關鍵是分布式,單機的可以使用ReentrantLock或者synchronized代碼塊來實現)
  2. 共享資源(各個系統訪問同一個資源,資源的載體可能是傳統關系型數據庫或者NoSQL)
  3. 同步訪問(即有很多個進程同事訪問同一個共享資源。沒有同步訪問,誰管你資源競爭不競爭)

二、應用的場景例子

  管理后台的部署架構(多台tomcat服務器+redis【多台tomcat服務器訪問一台redis】+mysql【多台tomcat服務器訪問一台服務器上的mysql】)就滿足使用分布式鎖的條件。多台服務器要訪問redis全局緩存的資源,如果不使用分布式鎖就會出現問題。

從redis獲取值N,對數值N進行邊界檢查,自加1,然后N寫回redis中。 這種應用場景很常見,像秒殺,全局遞增ID、IP訪問限制等。以IP訪問限制來說,惡意攻擊者可能發起無限次訪問,並發量比較大,分布式環境下對N的邊界檢查就不可靠,因為從redis讀的N可能已經是臟數據。傳統的加鎖的做法(如java的synchronized和Lock)也沒用,因為這是分布式環境,這個同步問題的救火隊員也束手無策。在這危急存亡之秋,分布式鎖終於有用武之地了。

  分布式鎖可以基於很多種方式實現,比如zookeeper、redis...。不管哪種方式,他的基本原理是不變的:用一個狀態值表示鎖,對鎖的占用和釋放通過狀態值來標識。

   這里主要講如何用redis實現分布式鎖。

 

三、使用redis的setNX命令實現分布式鎖

1、實現的原理

  Redis為單進程單線程模式,采用隊列模式將並發訪問變成串行訪問,且多客戶端對Redis的連接並不存在競爭關系。redis的SETNX命令可以方便的實現分布式鎖。

2、基本命令解析

1)setNX(SET if Not eXists)

語法:

SETNX key value

將 key 的值設為 value ,當且僅當 key 不存在。

若給定的 key 已經存在,則 SETNX 不做任何動作。

SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫

返回值:

  設置成功,返回  1 。
  設置失敗,返回  0 。

 

redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 設置成功
(integer) 1

redis> SETNX job "code-farmer"   # 嘗試覆蓋 job ,失敗
(integer) 0

redis> GET job                   # 沒有被覆蓋
"programmer"

 所以我們使用執行下面的命令

SETNX lock.foo <current Unix time + lock timeout + 1> 
  • 如返回1,則該客戶端獲得鎖,把lock.foo的鍵值設置為時間值表示該鍵已被鎖定,該客戶端最后可以通過DEL lock.foo來釋放該鎖。

  • 如返回0,表明該鎖已被其他客戶端取得,這時我們可以先返回或進行重試等對方完成或等待鎖超時。

2)getSET

語法:

GETSET key value

  將給定 key 的值設為 value ,並返回 key 的舊值(old value)。

  當 key 存在但不是字符串類型時,返回一個錯誤。

返回值:

  返回給定  key 的舊值。
  當  key 沒有舊值時,也即是,  key 不存在時,返回  nil 。
3)get
語法:
GET key

 返回值:

  當  key 不存在時,返回  nil ,否則,返回  key 的值。
  如果  key 不是字符串類型,那么返回一個錯誤
 
四、解決死鎖

上面的鎖定邏輯有一個問題:如果一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎么解決

我們可以通過鎖的鍵對應的時間戳來判斷這種情況是否發生了,如果當前的時間已經大於lock.foo的值,說明該鎖已失效,可以被重新使用。 

  發生這種情況時,可不能簡單的通過DEL來刪除鎖,然后再SETNX一次(講道理,刪除鎖的操作應該是鎖擁有這執行的,這里只需要等它超時即可),當多個客戶端檢測到鎖超時后都會嘗試去釋放它,這里就可能出現一個競態條件,讓我們模擬一下這個場景: 

復制代碼
C0操作超時了,但它還持有着鎖,C1和C2讀取lock.foo檢查時間戳,先后發現超時了。 
C1 發送DEL lock.foo 
C1 發送SETNX lock.foo 並且成功了。 
C2 發送DEL lock.foo 
C2 發送SETNX lock.foo 並且成功了。 
這樣一來,C1,C2都拿到了鎖!問題大了! 
復制代碼

  幸好這種問題是可以避免的,讓我們來看看C3這個客戶端是怎樣做的: 

復制代碼
C3發送SETNX lock.foo 想要獲得鎖,由於C0還持有鎖,所以Redis返回給C3一個0 
C3發送GET lock.foo 以檢查鎖是否超時了,如果沒超時,則等待或重試。 
反之,如果已超時,C3通過下面的操作來嘗試獲得鎖: 
GETSET lock.foo <current Unix time + lock timeout + 1> 
通過GETSET,C3拿到的時間戳如果仍然是超時的,那就說明,C3如願以償拿到鎖了。 
如果在C3之前,有個叫C4的客戶端比C3快一步執行了上面的操作,那么C3拿到的時間戳是個未超時的值,這時,C3沒有如期獲得鎖,需要再次等待或重試。留意一下,盡管C3沒拿到鎖,但它改寫了C4設置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。 
復制代碼

  注意:為了讓分布式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作,因為可能客戶端因為某個耗時的操作而掛起,操作完的時候鎖因為超時已經被別人獲得,這時就不必解鎖了。

五、代碼實現

expireMsecs 鎖持有超時,防止線程在入鎖以后,無限的執行下去,讓鎖無法釋放 
timeoutMsecs 鎖等待超時,防止線程飢餓,永遠沒有入鎖執行代碼的機會 

注意:項目里面需要先搭建好redis的相關配置

package test.miaosha;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

 
public class RedisLock {

    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);

    private RedisTemplate<String,Object> redisTemplate;

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    /**
     * Lock key path.
     */
    private String lockKey;

    /**
     * 鎖超時時間,防止線程在入鎖以后,無限的執行等待
     */
    private int expireMsecs = 60 * 1000;

    /**
     * 鎖等待時間,防止線程飢餓
     */
    private int timeoutMsecs = 10 * 1000;

    private volatile boolean locked = false;

    /**
     * Detailed constructor with default acquire timeout 10000 msecs and lock
     * expiration of 60000 msecs.
     *
     * @param lockKey
     *            lock key (ex. account:1, ...)
     */
    public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * Detailed constructor with default lock expiration of 60000 msecs.
     *
     */
    public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs) {
        this(redisTemplate, lockKey);
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     * Detailed constructor.
     *
     */
    public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
        this(redisTemplate, lockKey, timeoutMsecs);
        this.expireMsecs = expireMsecs;
    }

    /**
     * @return lock key
     */
    public String getLockKey() {
        return lockKey;
    }

    public String get(final String key) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.get(serializer.serialize(key));
                    connection.close();
                    if (data == null) {
                        return null;
                    }
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            logger.error("get redis error, key : {}", key);
        }
        return obj != null ? obj.toString() : null;
    }
    
    
    public String set(final String key,final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    connection.set(serializer.serialize(key), serializer.serialize(value));
                    return serializer;
                }
            });
        } catch (Exception e) {
            logger.error("get redis error, key : {}", key);
        }
        return obj != null ? obj.toString() : null;
    }

    public boolean setNX(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            logger.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (Boolean) obj : false;
    }

    private String getSet(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(ret);
                }
            });
        } catch (Exception e) {
            logger.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (String) obj : null;
    }

    /**
     * 獲得 lock. 實現思路: 主要是使用了redis 的setnx命令,緩存了鎖. reids緩存的key是鎖的key,所有的共享,
     * value是鎖的到期時間(注意:這里把過期時間放在value了,沒有時間上設置其超時時間) 執行過程:
     * 1.通過setnx嘗試設置某個key的值,成功(當前沒有這個鎖)則返回,成功獲得鎖
     * 2.鎖已經存在則獲取鎖的到期時間,和當前時間比較,超時的話,則設置新的值
     *
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException
     *             in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = String.valueOf(expires); // 鎖到期時間
            if (this.setNX(lockKey, expiresStr)) {
                // lock acquired
                locked = true;
                return true;
            }

            String currentValueStr = this.get(lockKey); // redis里的時間
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                // 判斷是否為空,不為空的情況下,如果被其他線程設置了值,則第二個條件判斷是過不去的
                // lock is expired

                String oldValueStr = this.getSet(lockKey, expiresStr);
                // 獲取上一個鎖到期時間,並設置現在的鎖到期時間,
                // 只有一個線程才能獲取上一個線上的設置時間,因為jedis.getSet是同步的
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    // 防止誤刪(覆蓋,因為key是相同的)了他人的鎖——這里達不到效果,這里值會被覆蓋,但是因為什么相差了很少的時間,所以可以接受

                    // [分布式的情況下]:如過這個時候,多個線程恰好都到了這里,但是只有一個線程的設置值和當前值相同,他才有權利獲取鎖
                    // lock acquired
                    locked = true;
                    return true;
                }
            }
            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

            /*
             * 延遲100 毫秒, 這里使用隨機時間可能會好一點,可以防止飢餓進程的出現,即,當同時到達多個進程,
             * 只會有一個進程獲得鎖,其他的都用同樣的頻率進行嘗試,后面有來了一些進行,也以同樣的頻率申請鎖,這將可能導致前面來的鎖得不到滿足.
             * 使用隨機的等待時間可以一定程度上保證公平性
             */
            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);

        }
        return false;
    }

    /**
     * Acqurired lock release.
     */
    public synchronized void unlock() {
        if (locked) {
            redisTemplate.delete(lockKey);
            locked = false;
        }
    }
    
    
    

}

MsService.java

package test.miaosha;

import org.springframework.data.redis.core.RedisTemplate;

public class MsService {
    
    /***
     * 搶購代碼
     * @param redisTemplate
     * @param key pronum 首先用客戶端設置數量
     * @return
     */
    public boolean seckill(RedisTemplate<String,Object> redisTemplate, String key) {
        RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000);
        try {
            if (lock.lock()) {
                // 需要加鎖的代碼
                String pronum=lock.get("pronum");

                //修改庫存
                if(Integer.parseInt(pronum)-1>=0) {
                    lock.set("pronum",String.valueOf(Integer.parseInt(pronum)-1));
                    System.out.println("庫存數量:"+pronum+"     成功!!!"+Thread.currentThread().getName());
                }else {
                    System.out.println("手慢拍大腿");
                }
                
                return true;
            } 

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 為了讓分布式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作,因為可能客戶端因為某個耗時的操作而掛起,
            // 操作完的時候鎖因為超時已經被別人獲得,這時就不必解鎖了。 ————這里沒有做
            lock.unlock();
        }
        return false;
    }
}

ThreadB.java

package test.miaosha;

import org.springframework.data.redis.core.RedisTemplate;

public class ThreadB extends Thread {
    private MsService service;
    private RedisTemplate<String,Object> redisTemplate;
    private String key;
 
    public ThreadB(MsService service,RedisTemplate<String,Object> redisTemplate,String key) {
        this.service = service;
        this.redisTemplate=redisTemplate;
        this.key=key;
    }
 
    @Override
    public void run() {
        service.seckill(redisTemplate, key);
    }
}
 

 測試類 直接開100個線程 模擬100個人搶購

package test.miaosha;

import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
 

public class MsTest {
    
    static ClassPathXmlApplicationContext appCtx = new ClassPathXmlApplicationContext("test/miaosha/spring-redis.xml");
    

     public static void main(String[] args) {
          RedisTemplate<String, Object> redisTemplate = (RedisTemplate<String, Object>)appCtx.getBean("redisTemplate",RedisTemplate.class);
          
          
          //RedisUtil redisUtil=(RedisUtil) appCtx.getBean("redisUtil");  
          System.out.println("開始");
          
            MsService service = new MsService();
            
            for (int i = 0; i < 100; i++) {
                ThreadB threadA = new ThreadB(service, redisTemplate, "MSKEY");
                threadA.start();
               
            }
           
    }
}

spring-redis.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd
      http://www.springframework.org/schema/mvc
      http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
        http://www.springframework.org/schema/util 
      http://www.springframework.org/schema/util/spring-util-3.0.xsd">

<!--[redis-JedisPoolConfig配置](http://blog.csdn.net/liang_love_java/article/details/50510753)-->
<!--    jedis-2.7.2.jar 依賴jar包 commons-pool2-2.3.jar 
        jedis基於 commons-pool2-2.3.jar 自己實現了一個資源池。
        配置參數 詳見 http://blog.csdn.net/liang_love_java/article/details/50510753
-->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> 
        <property name="maxIdle" value="8" /> 
        <property name="maxTotal" value="200" /> 
        <property name="blockWhenExhausted" value="true" /> 
        <property name="maxWaitMillis" value="30000" /> 
        <property name="testOnBorrow" value="true" />  
    </bean> 

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> 
        <property name="hostName" value="127.0.0.1" /> 
        <property name="port" value="6379"/> 
        <property name="poolConfig" ref="jedisPoolConfig" /> 
        <property name="usePool" value="true"/> 
    </bean> 

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">   
        <property name="connectionFactory"   ref="jedisConnectionFactory" />   
        <property name="keySerializer">   
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />   
        </property>      
        <property name="valueSerializer">   
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />   
        </property>   
        <property name="hashKeySerializer">     
           <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>     
        </property>   
        <property name="hashValueSerializer">   
           <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>     
        </property> 
     </bean> 
     
     <!--自定義redis工具類,在需要緩存的地方注入此類  -->  
    

</beans>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM