(轉載)使用redis實現分佈式鎖

最近在項目中遇到了類似“秒殺”的業務場景,在本篇博客中,我將用一個非常簡單的demo,闡述實現所謂“秒殺”的基本思路。

業務場景
所謂秒殺,從業務角度看,是短時間內多個用戶“爭搶”資源,這裏的資源在大部分秒殺場景裏是商品;將業務抽象,技術角度看,秒殺就是多個線程對資源進行操作,所以實現秒殺,就必須控制線程對資源的爭搶,既要保證高效併發,也要保證操作的正確。

一些可能的實現
剛纔提到過,實現秒殺的關鍵點是控制線程對資源的爭搶,根據基本的線程知識,可以不加思索的想到下面的一些方法: 
1、秒殺在技術層面的抽象應該就是一個方法,在這個方法裏可能的操作是將商品庫存-1,將商品加入用戶的購物車等等,在不考慮緩存的情況下應該是要操作數據庫的。那麼最簡單直接的實現就是在這個方法上加上synchronized關鍵字,通俗的講就是鎖住整個方法; 
2、鎖住整個方法這個策略簡單方便,但是似乎有點粗暴。可以稍微優化一下,只鎖住秒殺的代碼塊,比如寫數據庫的部分; 
3、既然有併發問題,那我就讓他“不併發”,將所有的線程用一個隊列管理起來,使之變成串行操作,自然不會有併發問題。

上面所述的方法都是有效的,但是都不好。爲什麼?第一和第二種方法本質上是“加鎖”,但是鎖粒度依然比較高。什麼意思?試想一下,如果兩個線程同時執行秒殺方法,這兩個線程操作的是不同的商品,從業務上講應該是可以同時進行的,但是如果採用第一二種方法,這兩個線程也會去爭搶同一個鎖,這其實是不必要的。第三種方法也沒有解決上面說的問題。

那麼如何將鎖控制在更細的粒度上呢?可以考慮爲每個商品設置一個互斥鎖,以和商品ID相關的字符串爲唯一標識,這樣就可以做到只有爭搶同一件商品的線程互斥,不會導致所有的線程互斥。分佈式鎖恰好可以幫助我們解決這個問題。

何爲分佈式鎖
分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。在分佈式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分佈式鎖。

我們來假設一個最簡單的秒殺場景:數據庫裏有一張表,column分別是商品ID,和商品ID對應的庫存量,秒殺成功就將此商品庫存量-1。現在假設有1000個線程來秒殺兩件商品,500個線程秒殺第一個商品,500個線程秒殺第二個商品。我們來根據這個簡單的業務場景來解釋一下分佈式鎖。 
通常具有秒殺場景的業務系統都比較複雜,承載的業務量非常巨大,併發量也很高。這樣的系統往往採用分佈式的架構來均衡負載。那麼這1000個併發就會是從不同的地方過來,商品庫存就是共享的資源,也是這1000個併發爭搶的資源,這個時候我們需要將併發互斥管理起來。這就是分佈式鎖的應用。 
而key-value存儲系統,如redis,因爲其一些特性,是實現分佈式鎖的重要工具。

=========================實現方案==============================================

@Component

public class RedisLock {

Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired private StringRedisTemplate redisTemplate;

/** * 加鎖 * @param key 商品id * @param value 當前時間+超時時間 * @return */

public boolean lock(String key, String value) {

if (redisTemplate.opsForValue().setIfAbsent(key, value)) {

//這個其實就是setnx命令,只不過在java這邊稍有變化,返回的是boolea return true;

}

//避免死鎖,且只讓一個線程拿到鎖

String currentValue = redisTemplate.opsForValue().get(key);

//如果鎖過期了

if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {

//獲取上一個鎖的時間

String oldValues = redisTemplate.opsForValue().getAndSet(key, value);

/* 只會讓一個線程拿到鎖 如果舊的value和currentValue相等,只會有一個線程達成條件,因爲第二個線程拿到的oldValue已經和currentValue不一樣了 */

if (!StringUtils.isEmpty(oldValues) && oldValues.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) {

logger.error("『redis分佈式鎖』解鎖異常,{}", e); }

}

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章