Redis鎖相關
君不見,高堂明鏡悲白發,朝如青絲暮成雪。
背景:面試的時候被問到有哪些鎖,很快脫口而出Volatile、Synchronized和ReentrantLock,也能講出他們之間的一些區別;當問到如在同一服務下同步鎖可以起作用,但是在不同的服務器上部署同一個微服務工程,然后用nginx作代理,很明顯,現在的線程鎖不管用了。分布式情況下如何保證線程同步?當時就答不上來了,分布式環境下我們需要換一把鎖,這把鎖必須和兩套甚至多套系統沒有任何的耦合度。可以使用Redis鎖實現分布式場景下的線程同步,使用Redies的API,如果key不存在,則設置一個key,這個key就是我們現在使用的一把鎖。每個線程到此處,先設置鎖,如果設置鎖失敗,則表明當前有線程獲取到了鎖,就返回。
一、單一服務器下的鎖
例如將商品的數量存到Redis中。每個用戶搶購前都需要到Redis中查詢商品數量(代替mysql數據庫。不考慮事務),如果商品數量大於0,則證明商品有庫存;然后我們在進行庫存扣減和接下來的操作;因為多線程並發問題,我們還需要在get()方法內部使用同步代碼塊,保證查詢庫存和減庫存操作的原子性。

1 import lombok.AllArgsConstructor; 2 import lombok.extern.slf4j.Slf4j; 3 import org.springframework.data.redis.core.RedisTemplate; 4 import org.springframework.web.bind.annotation.GetMapping; 5 import org.springframework.web.bind.annotation.RequestHeader; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.RestController; 8
9 @RestController 10 @AllArgsConstructor 11 @RequestMapping("/redis") 12 @Slf4j 13 public class TryRedisLock { 14
15 private RedisTemplate<String, String> redisTemplate; 16
17 @GetMapping(value = "/try/buy") 18 public String get(@RequestHeader(required = false) String userId) { 19 synchronized (this) { // 單機同步
20 String bird = redisTemplate.opsForValue().get("bird"); 21 Integer count = Integer.valueOf(bird); 22 if (count > 0) { 23 // 減庫存
24 redisTemplate.opsForValue().set("bird", String.valueOf(count - 1)); 25 log.info("用戶{}, 搶到了, {} 號商品!", userId, bird); 26 } 27 return "零庫存"; 28 } 29 } 30 }
二、分布式場景redis鎖
分布式場景下使用redis鎖需要注意如下幾個問題:
- 一台服務器宕機,導致無法釋放鎖;可以在try-catch的finally中釋放鎖或者給每一把鎖加過期時間。
- 任務線程未執行完畢但鎖已失效;可以延長鎖的過期時間,使用定時器防止key過期。
- 使用Redisson框架簡化代碼;
getLock()
方法代替了Redis的setIfAbsent()
,lock()
設置過期時間,最終我們在交易結束后釋放鎖;延長鎖的操作則由Redisson框架替我們完成,它會使用輪詢去查看key是否過期,在交易沒有完成時,自動重設Redis的key過期時間。

1 import lombok.AllArgsConstructor; 2 import lombok.extern.slf4j.Slf4j; 3 import org.springframework.data.redis.core.RedisTemplate; 4 import org.springframework.web.bind.annotation.GetMapping; 5 import org.springframework.web.bind.annotation.RequestHeader; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.RestController; 8
9 @RestController 10 @AllArgsConstructor 11 @RequestMapping("/redis") 12 @Slf4j 13 public class TryRedisLock { 14
15 private RedisTemplate<String, String> redisTemplate; 16
17
18 @GetMapping(value = "/try/again/buy") 19 public String getJudge(@RequestHeader(required = false) String userId) { 20 // 每個線程到此處,先設置鎖
21 /**
22 * 使用Redies的API如果key不存在,則設置一個key。這個key就是我們現在使用的一把鎖 23 * 每個線程到此處,先設置鎖 24 * 如果設置鎖失敗,則表明當前有線程獲取到了鎖,就返回。 25 */
26 Boolean birdLock = redisTemplate.opsForValue().setIfAbsent("birdLock", ""); 27 if (!birdLock) { 28 return ""; 29 } 30 try { 31 String bird = redisTemplate.opsForValue().get("bird"); 32 Integer count = Integer.valueOf(bird); 33 if (count > 0) { 34 redisTemplate.opsForValue().set("bird", String.valueOf(count - 1)); 35 log.info("用戶{}, 搶到了, {} 號商品!", userId, bird); 36 } 37 } finally { 38 redisTemplate.delete("birdLock"); // 刪除鎖
39 } 40 return ""; 41 } 42
43 @GetMapping(value = "/try/buy") 44 public String get(@RequestHeader(required = false) String userId) { 45 synchronized (this) { // 單機同步
46 String bird = redisTemplate.opsForValue().get("bird"); 47 Integer count = Integer.valueOf(bird); 48 if (count > 0) { 49 // 減庫存
50 redisTemplate.opsForValue().set("bird", String.valueOf(count - 1)); 51 log.info("用戶{}, 搶到了, {} 號商品!", userId, bird); 52 } 53 return "零庫存"; 54 } 55 } 56 }
君不見
高堂明鏡悲白發
朝如青絲暮成雪