分布式鎖學習筆記
分布式鎖,是指在分布式的集群環境中,保證不同節點的線程同步執行。
分布式鎖的實現有哪些?
1.Memcached分布式鎖
利用Memcached的add命令。此命令是原子性操作,只有在key不存在的情況下,才能add成功,也就意味著線程得到了鎖。
2.Redis分布式鎖
和Memcached的方式類似,利用Redis的setnx命令。此命令同樣是原子性操作,只有在key不存在的情況下,才能set成功。(setnx命令並不完善,後續會介紹替代方案)
3.Zookeeper分布式鎖
利用Zookeeper的順序臨時節點,來實現分布式鎖和等待隊列。Zookeeper設計的初衷,就是為了實現分布式鎖服務的。
4.Chubby
Google公司實現的粗粒度分布式鎖服務,底層利用了Paxos一致性算法。
這麽多種實現方法,選擇比較有代表性的Redis的分布式鎖來學習:
如何用Redis實現分布式鎖?
Redis分布式鎖的基本流程並不難理解,但要想寫得盡善盡美,也並不是那麽容易。在這裏,我們需要先了解分布式鎖實現的三個核心要素:
1.加鎖
最簡單的方法是使用setnx命令。key是鎖的唯一標識,按業務來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給key命名為 “lock_sale_商品ID” 。而value設置成什麽呢?我們可以姑且設置成1。加鎖的偽代碼如下:
setnx(key,1)
當一個線程執行setnx返回1,說明key原本不存在,該線程成功得到了鎖;當一個線程執行setnx返回0,說明key已經存在,該線程搶鎖失敗。
2.解鎖
有加鎖就得有解鎖。當得到鎖的線程執行完任務,需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執行del指令,偽代碼如下:
del(key)
釋放鎖之後,其他線程就可以繼續執行setnx命令來獲得鎖。
3.鎖超時
鎖超時是什麽意思呢?如果一個得到鎖的線程在執行任務的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住,別的線程再也別想進來。
所以,setnx的key必須設置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。setnx不支持超時參數,所以需要額外的指令,偽代碼如下:
expire(key, 30)
模擬此場景,寫一個搶購秒殺的demo:
Controller:
/** * 秒殺,搶到了會返回剩余的庫存量 * @param productId * @return * @throws Exception */ @GetMapping("/order/{productId}") public String skill(@PathVariable String productId)throws Exception { log.info("@skill request, productId:" + productId); secKillService.orderProductMockDiffUser(productId); return secKillService.querySecKillProductInfo(productId); }
業務層Impl:(未做任何同步處理)
@Service public class SecKillServiceImpl implements SecKillService { private static final int TIMEOUT = 10 * 1000; //超時時間 10s /** * 國慶活動,皮蛋粥特價,限量100000份 */ 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("123456", 100000); stock.put("123456", 100000); } 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 orderProductMockDiffUser(String productId) { Long time = System.currentTimeMillis() + TIMEOUT; //1.查詢該商品庫存,為0則活動結束。 int stockNum = stock.get(productId); if(stockNum == 0) { throw new SellException(100,"活動結束"); }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 bench 壓測:ab -n 100 -c 100 http://localhost:8080/skill/order/123456
發現數據同步失敗:
接下來嘗試在函數加上 synchronized,同步沒問題,但是響應時間較長
使用Redis分布式鎖:(需要引入 spring-boot-starter-data-redis 相關依賴)
RedisLock類:
@Component @Slf4j public class RedisLock { @Autowired private StringRedisTemplate redisTemplate; /** * 加鎖 * @param key * @param value * @return */ public boolean lock(String key, String value){ // 設置redis值,如果值已存在不做操作,跳到下一步 if (redisTemplate.opsForValue().setIfAbsent(key, value)) { return true; } // 獲取reids中的時間戳 String currentValue = redisTemplate.opsForValue().get(key); if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) { // 拿到上一次的時間戳,並設置新的時間戳,保證只有一個線程能同步 String oldValue = redisTemplate.opsForValue().getAndSet(key, value); // 若第二個線程進來,此時oldvalue已經不等於currentValue了 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) && value.equals(currentValue)) { redisTemplate.opsForValue().getOperations().delete(key); } } catch (Exception e) { log.error("【redis分布式鎖】解鎖異常, {}", e); } } }
業務層Impl:(加上Redis鎖的處理)
@Service public class SecKillServiceImpl implements SecKillService { private static final int TIMEOUT = 10 * 1000; //超時時間 10s @Autowired private RedisLock redisLock; /** * 國慶活動,皮蛋粥特價,限量100000份 */ 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("123456", 100000); stock.put("123456", 100000); } 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 orderProductMockDiffUser(String productId) { Long time = System.currentTimeMillis() + TIMEOUT; //加鎖 if (!redisLock.lock(productId, String.valueOf(time))) { throw new SellException(101, "人太多了歇一會吧!"); } //1.查詢該商品庫存,為0則活動結束。 int stockNum = stock.get(productId); if(stockNum == 0) { throw new SellException(100,"活動結束"); }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)); } }
重啟,再次用apache bench壓測 ab -n 100 -c 100 http://localhost:8080/skill/order/123456
結果,響應時間非常快,減少了卡頓,同步也正常!
分布式鎖學習筆記