1. 程式人生 > >用redis操作快取來實現分散式鎖例項

用redis操作快取來實現分散式鎖例項

目前幾乎所有的大型網站及應用都是採用分散式部署的方式,分散式系統開發帶來的優點很多,高可用,高併發,水平擴充套件,分開部署等。但分散式的開發也帶來了一些新問題,有的時候,我們需要保證一個方法在同一時間內只能被同一個執行緒執行。在單機環境中,Java中其實提供了很多併發處理相關的API  ,也就是我們常說的“鎖”(如synchronized,lock),但是這些API在分散式場景中就無能為力了,也就是說Java沒有提供分散式鎖的功能。

基於分散式鎖的實現有多種方案,常見的基於資料庫本身的鎖來實現,或者基於zookeeper的API實現,或者是基於快取來實現分散式鎖等等,這些方案都各有可取之處,今天我們介紹的是基於redis的快取實現分散式鎖的方案,大家如果對其他方案有興趣的可以上網搜尋研究。

redis是基於key-value的一種NoSql資料庫,廣泛應用於分散式的應用中,一般用於放置快取資料。安裝的方法也比較簡單,樓主安裝的是windows版本的,選擇最新的zip版,下載完之後直接解壓即可。下載地址:https://github.com/MicrosoftArchive/redis/tags

redis中有一個命令setnx (SET IF NOT EXISTS) , 如果不存在,就設定key,將 key 的值設為 value,當且僅當 key 不存在。若給定的 key 已經存在,則 SETNX 不做任何動作。基於這個特性,我們可以對需要鎖住的物件加上key,這樣,同一時間就只能有一個執行緒擁有這把鎖,從而達到分散式鎖的效果。

下面用一個具體的Java例項來展示redis的分散式鎖效果。

Java操作redis需要用到第三方的庫類,所以先在pom.xml中引入依賴。


加入依賴後,做一個redis的工具方法,分別實現的是加鎖和解鎖的功能。

public class RedisLock {

    @Autowired
private StringRedisTemplate redisTemplate;
/**
     * 加鎖
     *
     * @param key
* @param value 當前時間+超時時間
     * @return
*/
public boolean lock(String key, 
String value) { //相當於setnx命令 if (redisTemplate.opsForValue().setIfAbsent(key, value)) { return true; } //下面的這段程式碼是判斷之前加的鎖是否超時,是的話就更新,一定要加這段程式碼 //不然就有可能出現死鎖。 String currentValue = redisTemplate.opsForValue().get(key); //如果鎖過期 if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) { //獲取上一個鎖的時間,這段程式碼的判斷是防止多執行緒進入這裡,只會有一個執行緒拿到鎖 String oldValue = redisTemplate.opsForValue().getAndSet(key, value); if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) { return true; } } return false; } /** * 解鎖 * * @param key * @param value */ public void unLock(String key, String value) { try { String currentValue = redisTemplate.opsForValue().get(key); if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) { redisTemplate.opsForValue().getOperations().delete(key); } } catch (Exception e) { log.error("【redis分散式鎖】 解鎖異常,{}", e); } } }

現在,我們模擬一個下單的場景,假設有一個秒殺的活動,同一時間有多個執行緒對同一個產品進行訪問,然後分別看看加鎖和沒加鎖的結果來做對比。下面是秒殺的模擬程式碼:

public class SecKillController {

    @Autowired
private SecKillService secKillService;
/**
     * 查詢秒殺活動特價商品的資訊
     * @param productId
* @return
* @throws Exception
     */
@GetMapping("/query/{productId}")
    public String query(@PathVariable String productId) throws Exception{
        return secKillService.querySecKillProductInfo(productId);
}

    /**
     * 秒殺的方法
     * @param productId
* @return
* @throws Exception
     */
@GetMapping("/order/{productId}")
    public String skill(@PathVariable String productId) throws Exception{
        log.info("@skill request ,productId:" +productId);
secKillService.orderProductKill(productId);
        return secKillService.querySecKillProductInfo(productId);
}
}
public class SecKillServiceImpl implements SecKillService {

    private static final int TIME_OUT = 1 * 1000;
@Autowired
private RedisLock redisLock;
    static Map<String, Integer> products;
    static Map<String, Integer> stock;
    static Map<String, String> orders;
    static {
        /**
         * 模擬多個表,商品資訊表,庫存表,秒殺成功訂單表
         */
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("123", 100000);
stock.put("123", 100000);
}

    /**
     * @param productId  訂單id
     * @return
*/
private String queryMap(String productId) {
        return "限量份數" + products.get(productId)
                + "還剩:" + stock.get(productId) + "份"
+ "該商品成功下單使用者數目:"
+ orders.size() + "人";
}

    @Override
public String querySecKillProductInfo(String productId) {
        return this.queryMap(productId);
}

    @Override
public void orderProductKill(String productId) {

        //1.查詢該商品庫存,為0則活動結束
int stockNum = stock.get(productId);
        if (stockNum == 0) {
            throw new RuntimeException("活動結束");
} else {
            //2.下單(模擬不同使用者openid不同)
orders.put(KeyUtil.getUniqueKey(), productId);
//3.減庫存
stockNum = stockNum - 1;
            try {
                Thread.sleep(100);
} catch (InterruptedException e) {
                e.printStackTrace();
}
            stock.put(productId, stockNum);
}
    }
}

先模擬沒加鎖的下單狀態,我們開啟工程後,用Apache ab作為壓測工具來模擬高併發訪問過程


在瀏覽器上訪問查詢後的訂單數量,結果顯示如下:


可以看到,再高併發的訪問環境下,如果我們沒有對訂單做鎖的處理,那麼就可能出現數據的紊亂,導致結果不對應,這顯然不符合我們的需求,下面我們來看看加上redis鎖之後的訪問情況,先把service中的秒殺程式碼加上鎖。

@Override
public void orderProductKill(String productId) {

    //加鎖,保證下面的程式碼單執行緒的訪問
long time = System.currentTimeMillis() + TIME_OUT;
    if (!redisLock.lock(productId, String.valueOf(time))) {
        throw new RuntimeException( "下單失敗");
}

    //1.查詢該商品庫存,為0則活動結束
int stockNum = stock.get(productId);
    if (stockNum == 0) {
        throw new RuntimeException("活動結束");
} else {
        //2.下單(模擬不同使用者openid不同)
orders.put(KeyUtil.getUniqueKey(), productId);
//3.減庫存
stockNum = stockNum - 1;
        try {
            Thread.sleep(100);
} catch (InterruptedException e) {
            e.printStackTrace();
}
        stock.put(productId, stockNum);
}
    //解鎖
redisLock.unLock(productId, String.valueOf(time));
}

 然後再進行同樣的操作


我們可以看到,加上鎖之後的訂單處理數量是正確的,也就是redis鎖是起到了作用的,這是符合我們的需求的。

上面的例子相對比較簡單,因為精力能力有限,樓主沒法給大家展示真正的分散式鎖的實現效果,但從原理上其實是一樣的,都是用redis的setnx命令來加上鎖,保證分散式環境下鎖住的物件只能被一個執行緒訪問,而且從實現方式上來說也比較簡單  (只需要一個命令就行,很深入人心得意 ) ,因此,redis在分散式鎖的應用中也被廣泛使用。