精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

加鎖了還有并發問題?Redis分布式鎖,真的用對了?

存儲 分布式 Redis
既然項目交付到手中,這樣的問題是必須要解決的。梳理了所有賬務處理邏輯,最終找到了原因:數據庫并發操作熱點賬戶導致。就這這個問題,來聊一聊分布式系統下基于Redis的分布式鎖。順便也分解一下問題形成原因及解決方案。

 [[431030]]

新接手的項目,偶爾會出現賬不平的問題。之前的技術老大臨走時給的解釋是:排查了,沒找到原因,之后太忙就沒再解決,可能是框架的原因……

既然項目交付到手中,這樣的問題是必須要解決的。梳理了所有賬務處理邏輯,最終找到了原因:數據庫并發操作熱點賬戶導致。就這這個問題,來聊一聊分布式系統下基于Redis的分布式鎖。順便也分解一下問題形成原因及解決方案。

原因分析

系統并發量并不高,存在熱點賬戶,但也不至于那么嚴重。問題的根源在于系統架構設計,人為的制造了并發。場景是這樣的:商戶批量導入一批數據,系統會進行前置處理,并對賬戶余額進行增減。

此時,另外一個定時任務,也會對賬戶進行掃描更新。而且對同一賬戶的操作分布到各個系統當中,熱點賬戶也就出現了。

針對此問題的解決方案,從架構層面可以考慮將賬務系統進行抽離,集中在一個系統中進行處理,所有的數據庫事務及執行順序由賬務系統來統籌處理。從技術方面來講,則可以通過鎖機制來對熱點賬戶進行加鎖。

本篇文章就針對熱點賬戶基于分布式鎖的實現方式進行詳細的講解。

鎖的分析

在Java的多線程環境下,通常有幾類鎖可以使用:

  • JVM內存模型級別的鎖,常用的有:synchronized、Lock等;
  • 數據庫鎖,比如樂觀鎖,悲觀鎖等;
  • 分布式鎖;

JVM內存級別的鎖,可以保證單體服務下線程的安全性,比如多個線程訪問/修改一個全局變量。但當系統進行集群部署時,JVM級別的本地鎖就無能為力了。

悲觀鎖與樂觀鎖

像上述案例中,熱點賬戶就屬于分布式系統中的共享資源,我們通常會采用數據庫鎖或分布式鎖來進行解決。

數據庫鎖,又分為樂觀鎖和悲觀鎖。

悲觀鎖是基于數據庫(Mysql的InnoDB)提供的排他鎖來實現的。在進行事務操作時,通過select ... for update語句,MySQL會對查詢結果集中每行數據都添加排他鎖,其他線程對該記錄的更新與刪除操作都會阻塞。從而達到共享資源的順序執行(修改);

樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設數據一般情況不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測。如果沖突則返回給用戶異常信息,讓用戶決定如何去做。樂觀鎖適用于讀多寫少的場景,這樣可以提高程序的吞吐量。在樂觀鎖實現時通常會基于記錄狀態或添加version版本來進行實現。

悲觀鎖失效場景

項目中使用了悲觀鎖,但悲觀鎖卻失效了。這也是使用悲觀鎖時,常見的誤區,下面來分析一下。

正常使用悲觀鎖的流程:

  • 通過select ... for update鎖定記錄;
  • 計算新余額,修改金額并存儲;
  • 執行完成釋放鎖;

經常犯錯的處理流程:

  • 查詢賬戶余額,計算新余額;
  • 通過select ... for update鎖定記錄;
  • 修改金額并存儲;
  • 執行完成釋放鎖;

錯誤的流程中,比如A和B服務查詢到的余額都是100,A扣減50,B扣減40,然后A鎖定記錄,更新數據庫為50;A釋放鎖之后,B鎖定記錄,更新數據庫為60。顯然,后者把前者的更新給覆蓋掉了。解決的方案就是擴大鎖的范圍,將鎖提前到計算新余額之前。

通常悲觀鎖對數據庫的壓力是非常大的,在實踐中通常會根據場景使用樂觀鎖或分布式鎖等方式來實現。

下面進入正題,講講基于Redis的分布式鎖實現。

Redis分布式鎖實戰演習

這里以Spring Boot、Redis、Lua腳本為例來演示分布式鎖的實現。為了簡化處理,示例中Redis既承擔了分布式鎖的功能,也承擔了數據庫的功能。

場景構建

集群環境下,對同一個賬戶的金額進行操作,基本步驟:

  • 從數據庫讀取用戶金額;
  • 程序修改金額;
  • 再將最新金額存儲到數據庫;
  • 下面從最初不加鎖,不同步處理,逐步推演出最終的分布式鎖。

基礎集成及類構建

準備一個不加鎖處理的基礎業務環境。

首先在Spring Boot項目中引入相關依賴:

  1. <dependency> 
  2.  <groupId>org.springframework.boot</groupId> 
  3.  <artifactId>spring-boot-starter-data-redis</artifactId> 
  4. </dependency> 
  5. <dependency> 
  6.  <groupId>org.springframework.boot</groupId> 
  7.  <artifactId>spring-boot-starter-web</artifactId> 
  8. </dependency> 

賬戶對應實體類UserAccount:

  1. public class UserAccount { 
  2.  
  3.  //用戶ID 
  4.  private String userId; 
  5.  //賬戶內金額 
  6.  private int amount; 
  7.  
  8.  //添加賬戶金額 
  9.  public void addAmount(int amount) { 
  10.   this.amount = this.amount + amount; 
  11.  } 
  12.  // 省略構造方法和getter/setter  

創建一個線程實現類AccountOperationThread:

  1. public class AccountOperationThread implements Runnable { 
  2.  
  3.  private final static Logger logger = LoggerFactory.getLogger(AccountOperationThread.class); 
  4.  
  5.  private static final Long RELEASE_SUCCESS = 1L; 
  6.  
  7.  private String userId; 
  8.  
  9.  private RedisTemplate<Object, Object> redisTemplate; 
  10.  
  11.  public AccountOperationThread(String userId, RedisTemplate<Object, Object> redisTemplate) { 
  12.   this.userId = userId; 
  13.   this.redisTemplate = redisTemplate; 
  14.  } 
  15.  
  16.  @Override 
  17.  public void run() { 
  18.   noLock(); 
  19.  } 
  20.  
  21.  /** 
  22.   * 不加鎖 
  23.   */ 
  24.  private void noLock() { 
  25.   try { 
  26.    Random random = new Random(); 
  27.    // 模擬線程進行業務處理 
  28.    TimeUnit.MILLISECONDS.sleep(random.nextInt(100) + 1); 
  29.   } catch (InterruptedException e) { 
  30.    e.printStackTrace(); 
  31.   } 
  32.   //模擬數據庫中獲取用戶賬號 
  33.   UserAccount userAccount = (UserAccount) redisTemplate.opsForValue().get(userId); 
  34.   // 金額+1 
  35.   userAccount.addAmount(1); 
  36.   logger.info(Thread.currentThread().getName() + " : user id : " + userId + " amount : " + userAccount.getAmount()); 
  37.   //模擬存回數據庫 
  38.   redisTemplate.opsForValue().set(userId, userAccount); 
  39.  } 

其中RedisTemplate的實例化交給了Spring Boot:

  1. @Configuration 
  2. public class RedisConfig { 
  3.  
  4.  @Bean 
  5.  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { 
  6.   RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); 
  7.   redisTemplate.setConnectionFactory(redisConnectionFactory); 
  8.   Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = 
  9.     new Jackson2JsonRedisSerializer<>(Object.class); 
  10.   ObjectMapper objectMapper = new ObjectMapper(); 
  11.   objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 
  12.   objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 
  13.   jackson2JsonRedisSerializer.setObjectMapper(objectMapper); 
  14.   // 設置value的序列化規則和 key的序列化規則 
  15.   redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); 
  16.   redisTemplate.setKeySerializer(new StringRedisSerializer()); 
  17.   redisTemplate.afterPropertiesSet(); 
  18.   return redisTemplate; 
  19.  } 

最后,再準備一個TestController來進行觸發多線程的運行:

  1. @RestController 
  2. public class TestController { 
  3.  
  4.  private final static Logger logger = LoggerFactory.getLogger(TestController.class); 
  5.  
  6.  private static ExecutorService executorService = Executors.newFixedThreadPool(10); 
  7.  
  8.  @Autowired 
  9.  private RedisTemplate<Object, Object> redisTemplate; 
  10.  
  11.  @GetMapping("/test"
  12.  public String test() throws InterruptedException { 
  13.   // 初始化用戶user_001到Redis,賬戶金額為0 
  14.   redisTemplate.opsForValue().set("user_001", new UserAccount("user_001", 0)); 
  15.   // 開啟10個線程進行同步測試,每個線程為賬戶增加1元 
  16.   for (int i = 0; i < 10; i++) { 
  17.    logger.info("創建線程i=" + i); 
  18.    executorService.execute(new AccountOperationThread("user_001", redisTemplate)); 
  19.   } 
  20.  
  21.   // 主線程休眠1秒等待線程跑完 
  22.   TimeUnit.MILLISECONDS.sleep(1000); 
  23.   // 查詢Redis中的user_001賬戶 
  24.   UserAccount userAccount = (UserAccount) redisTemplate.opsForValue().get("user_001"); 
  25.   logger.info("user id : " + userAccount.getUserId() + " amount : " + userAccount.getAmount()); 
  26.   return "success"
  27.  } 

執行上述程序,正常來說10個線程,每個線程加1,結果應該是10。但多執行幾次,會發現,結果變化很大,基本上都要比10小。

  1. [pool-1-thread-5] c.s.redis.thread.AccountOperationThread  : pool-1-thread-5 : user id : user_001 amount : 1 
  2. [pool-1-thread-4] c.s.redis.thread.AccountOperationThread  : pool-1-thread-4 : user id : user_001 amount : 1 
  3. [pool-1-thread-3] c.s.redis.thread.AccountOperationThread  : pool-1-thread-3 : user id : user_001 amount : 1 
  4. [pool-1-thread-1] c.s.redis.thread.AccountOperationThread  : pool-1-thread-1 : user id : user_001 amount : 1 
  5. [pool-1-thread-1] c.s.redis.thread.AccountOperationThread  : pool-1-thread-1 : user id : user_001 amount : 2 
  6. [pool-1-thread-2] c.s.redis.thread.AccountOperationThread  : pool-1-thread-2 : user id : user_001 amount : 2 
  7. [pool-1-thread-5] c.s.redis.thread.AccountOperationThread  : pool-1-thread-5 : user id : user_001 amount : 2 
  8. [pool-1-thread-4] c.s.redis.thread.AccountOperationThread  : pool-1-thread-4 : user id : user_001 amount : 3 
  9. [pool-1-thread-1] c.s.redis.thread.AccountOperationThread  : pool-1-thread-1 : user id : user_001 amount : 4 
  10. [pool-1-thread-3] c.s.redis.thread.AccountOperationThread  : pool-1-thread-3 : user id : user_001 amount : 5 
  11. [nio-8080-exec-1] c.s.redis.controller.TestController      : user id : user_001 amount : 5 

以上述日志為例,前四個線程都將值改為1,也就是后面三個線程都將前面的修改進行了覆蓋,導致最終結果不是10,只有5。這顯然是有問題的。

Redis同步鎖實現

針對上面的情況,在同一個JVM當中,我們可以通過線程加鎖來完成。但在分布式環境下,JVM級別的鎖是沒辦法實現的,這里可以采用Redis同步鎖實現。

基本思路:第一個線程進入時,在Redis中進記錄,當后續線程過來請求時,判斷Redis是否存在該記錄,如果存在則說明處于鎖定狀態,進行等待或返回。如果不存在,則進行后續業務處理。

  1. /** 
  2.  * 1.搶占資源時判斷是否被鎖。 
  3.  * 2.如未鎖則搶占成功且加鎖,否則等待鎖釋放。 
  4.  * 3.業務完成后釋放鎖,讓給其它線程。 
  5.  * <p> 
  6.  * 該方案并未解決同步問題,原因:線程獲得鎖和加鎖的過程,并非原子性操作,可能會導致線程A獲得鎖,還未加鎖時,線程B也獲得了鎖。 
  7.  */ 
  8. private void redisLock() { 
  9.  Random random = new Random(); 
  10.  try { 
  11.   TimeUnit.MILLISECONDS.sleep(random.nextInt(1000) + 1); 
  12.  } catch (InterruptedException e) { 
  13.   e.printStackTrace(); 
  14.  } 
  15.  while (true) { 
  16.   Object lock = redisTemplate.opsForValue().get(userId + ":syn"); 
  17.   if (lock == null) { 
  18.    // 獲得鎖 -> 加鎖 -> 跳出循環 
  19.    logger.info(Thread.currentThread().getName() + ":獲得鎖"); 
  20.    redisTemplate.opsForValue().set(userId + ":syn""lock"); 
  21.    break; 
  22.   } 
  23.   try { 
  24.    // 等待500毫秒重試獲得鎖 
  25.    TimeUnit.MILLISECONDS.sleep(500); 
  26.   } catch (InterruptedException e) { 
  27.    e.printStackTrace(); 
  28.   } 
  29.  } 
  30.  try { 
  31.   //模擬數據庫中獲取用戶賬號 
  32.   UserAccount userAccount = (UserAccount) redisTemplate.opsForValue().get(userId); 
  33.   if (userAccount != null) { 
  34.    //設置金額 
  35.    userAccount.addAmount(1); 
  36.    logger.info(Thread.currentThread().getName() + " : user id : " + userId + " amount : " + userAccount.getAmount()); 
  37.    //模擬存回數據庫 
  38.    redisTemplate.opsForValue().set(userId, userAccount); 
  39.   } 
  40.  } finally { 
  41.   //釋放鎖 
  42.   redisTemplate.delete(userId + ":syn"); 
  43.   logger.info(Thread.currentThread().getName() + ":釋放鎖"); 
  44.  } 

在while代碼塊中,先判斷對應用戶ID是否在Redis中存在,如果不存在,則進行set加鎖,如果存在,則跳出循環繼續等待。

上述代碼,看起來實現了加鎖的功能,但當執行程序時,會發現與未加鎖一樣,依舊存在并發問題。原因是:獲取鎖和加鎖的操作并不是原子的。比如兩個線程發現lock都是null,都進行了加鎖,此時并發問題依舊存在。

Redis原子性同步鎖

針對上述問題,可將獲取鎖和加鎖的過程原子化處理。基于spring-boot-data-redis提供的原子化API可以實現:

  1. // 該方法使用了redis的指令:SETNX key value 
  2. // 1.key不存在,設置成功返回value,setIfAbsent返回true; 
  3. // 2.key存在,則設置失敗返回null,setIfAbsent返回false; 
  4. // 3.原子性操作; 
  5. Boolean setIfAbsent(K var1, V var2); 

上述方法的原子化操作是對Redis的setnx命令的封裝,在Redis中setnx的使用如下實例:

  1. redis> SETNX mykey "Hello" 
  2. (integer) 1 
  3. redis> SETNX mykey "World" 
  4. (integer) 0 
  5. redis> GET mykey 
  6. "Hello" 

第一次,設置mykey時,并不存在,則返回1,表示設置成功;第二次設置mykey時,已經存在,則返回0,表示設置失敗。再次查詢mykey對應的值,會發現依舊是第一次設置的值。也就是說redis的setnx保證了唯一的key只能被一個服務設置成功。

理解了上述API及底層原理,來看看線程中的實現方法代碼如下:

  1. /** 
  2.  * 1.原子操作加鎖 
  3.  * 2.競爭線程循環重試獲得鎖 
  4.  * 3.業務完成釋放鎖 
  5.  */ 
  6. private void atomicityRedisLock() { 
  7.  //Spring data redis 支持的原子性操作 
  8.  while (!redisTemplate.opsForValue().setIfAbsent(userId + ":syn""lock")) { 
  9.   try { 
  10.    // 等待100毫秒重試獲得鎖 
  11.    TimeUnit.MILLISECONDS.sleep(100); 
  12.   } catch (InterruptedException e) { 
  13.    e.printStackTrace(); 
  14.   } 
  15.  } 
  16.  logger.info(Thread.currentThread().getName() + ":獲得鎖"); 
  17.  try { 
  18.   //模擬數據庫中獲取用戶賬號 
  19.   UserAccount userAccount = (UserAccount) redisTemplate.opsForValue().get(userId); 
  20.   if (userAccount != null) { 
  21.    //設置金額 
  22.    userAccount.addAmount(1); 
  23.    logger.info(Thread.currentThread().getName() + " : user id : " + userId + " amount : " + userAccount.getAmount()); 
  24.    //模擬存回數據庫 
  25.    redisTemplate.opsForValue().set(userId, userAccount); 
  26.   } 
  27.  } finally { 
  28.   //釋放鎖 
  29.   redisTemplate.delete(userId + ":syn"); 
  30.   logger.info(Thread.currentThread().getName() + ":釋放鎖"); 
  31.  } 

再次執行代碼,會發現結果正確了,也就是說可以成功的對分布式線程進行了加鎖。

Redis分布式鎖的死鎖

雖然上述代碼執行結果沒問題,但如果應用異常宕機,沒來得及執行finally中釋放鎖的方法,那么其他線程則永遠無法獲得這個鎖。

此時可采用setIfAbsent的重載方法:

  1. Boolean setIfAbsent(K var1, V var2, long var3, TimeUnit var5); 

基于該方法,可以設置鎖的過期時間。這樣即便獲得鎖的線程宕機,在Redis中數據過期之后,其他線程可正常獲得該鎖。

示例代碼如下:

  1. private void atomicityAndExRedisLock() { 
  2.   try { 
  3.    //Spring data redis 支持的原子性操作,并設置5秒過期時間 
  4.    while (!redisTemplate.opsForValue().setIfAbsent(userId + ":syn"
  5.      System.currentTimeMillis() + 5000, 5000, TimeUnit.MILLISECONDS)) { 
  6.     // 等待100毫秒重試獲得鎖 
  7.     logger.info(Thread.currentThread().getName() + ":嘗試循環獲取鎖"); 
  8.     TimeUnit.MILLISECONDS.sleep(1000); 
  9.    } 
  10.    logger.info(Thread.currentThread().getName() + ":獲得鎖--------"); 
  11.    // 應用在這里宕機,進程退出,無法執行 finally; 
  12.    Thread.currentThread().interrupt(); 
  13.    // 業務邏輯... 
  14.   } catch (InterruptedException e) { 
  15.    e.printStackTrace(); 
  16.   } finally { 
  17.    //釋放鎖 
  18.    if (!Thread.currentThread().isInterrupted()) { 
  19.     redisTemplate.delete(userId + ":syn"); 
  20.     logger.info(Thread.currentThread().getName() + ":釋放鎖"); 
  21.    } 
  22.   } 
  23.  } 

業務超時及守護線程

上面添加了Redis所的超時時間,看似解決了問題,但又引入了新的問題。

比如,正常情況下線程A在5秒內可正常處理完業務,但偶發會出現超過5秒的情況。如果將超時時間設置為5秒,線程A獲得了鎖,但業務邏輯處理需要6秒。此時,線程A還在正常業務邏輯,線程B已經獲得了鎖。當線程A處理完時,有可能將線程B的鎖給釋放掉。

在上述場景中有兩個問題點:

  • 第一,線程A和線程B可能會同時在執行,存在并發問題。
  • 第二,線程A可能會把線程B的鎖給釋放掉,導致一系列的惡性循環。

當然,可以通過在Redis中設置value值來判斷鎖是屬于線程A還是線程B。但仔細分析會發現,這個問題的本質是因為線程A執行業務邏輯耗時超出了鎖超時的時間。

那么就有兩個解決方案了:

  • 第一,將超時時間設置的足夠長,確保業務代碼能夠在鎖釋放之前執行完成;
  • 第二,為鎖添加守護線程,為將要過期釋放但未釋放的鎖增加時間;

第一種方式需要全行大多數情況下業務邏輯的耗時,進行超時時間的設定。

第二種方式,可通過如下守護線程的方式來動態增加鎖超時時間。

  1. public class DaemonThread implements Runnable { 
  2.  private final static Logger logger = LoggerFactory.getLogger(DaemonThread.class); 
  3.  
  4.  // 是否需要守護 主線程關閉則結束守護線程 
  5.  private volatile boolean daemon = true
  6.  // 守護鎖 
  7.  private String lockKey; 
  8.  
  9.  private RedisTemplate<Object, Object> redisTemplate; 
  10.  
  11.  public DaemonThread(String lockKey, RedisTemplate<Object, Object> redisTemplate) { 
  12.   this.lockKey = lockKey; 
  13.   this.redisTemplate = redisTemplate; 
  14.  } 
  15.  
  16.  @Override 
  17.  public void run() { 
  18.   try { 
  19.    while (daemon) { 
  20.     long time = redisTemplate.getExpire(lockKey, TimeUnit.MILLISECONDS); 
  21.     // 剩余有效期小于1秒則續命 
  22.     if (time < 1000) { 
  23.      logger.info("守護進程: " + Thread.currentThread().getName() + " 延長鎖時間 5000 毫秒"); 
  24.      redisTemplate.expire(lockKey, 5000, TimeUnit.MILLISECONDS); 
  25.     } 
  26.     TimeUnit.MILLISECONDS.sleep(300); 
  27.    } 
  28.    logger.info(" 守護進程: " + Thread.currentThread().getName() + "關閉 "); 
  29.   } catch (InterruptedException e) { 
  30.    e.printStackTrace(); 
  31.   } 
  32.  } 
  33.  
  34.  // 主線程主動調用結束 
  35.  public void stop() { 
  36.   daemon = false
  37.  } 

上述線程每隔300毫秒獲取一下Redis中鎖的超時時間,如果小于1秒,則延長5秒。當主線程調用關閉時,守護線程也隨之關閉。

主線程中相關代碼實現:

  1. private void deamonRedisLock() { 
  2.   //守護線程 
  3.   DaemonThread daemonThread = null
  4.   //Spring data redis 支持的原子性操作,并設置5秒過期時間 
  5.   String uuid = UUID.randomUUID().toString(); 
  6.   String value = Thread.currentThread().getId() + ":" + uuid; 
  7.   try { 
  8.    while (!redisTemplate.opsForValue().setIfAbsent(userId + ":syn", value, 5000, TimeUnit.MILLISECONDS)) { 
  9.     // 等待100毫秒重試獲得鎖 
  10.     logger.info(Thread.currentThread().getName() + ":嘗試循環獲取鎖"); 
  11.     TimeUnit.MILLISECONDS.sleep(1000); 
  12.    } 
  13.    logger.info(Thread.currentThread().getName() + ":獲得鎖----"); 
  14.    // 開啟守護線程 
  15.    daemonThread = new DaemonThread(userId + ":syn", redisTemplate); 
  16.    Thread thread = new Thread(daemonThread); 
  17.    thread.start(); 
  18.    // 業務邏輯執行10秒... 
  19.    TimeUnit.MILLISECONDS.sleep(10000); 
  20.   } catch (InterruptedException e) { 
  21.    e.printStackTrace(); 
  22.   } finally { 
  23.    //釋放鎖 這里也需要原子操作,今后通過 Redis + Lua 講 
  24.    String result = (String) redisTemplate.opsForValue().get(userId + ":syn"); 
  25.    if (value.equals(result)) { 
  26.     redisTemplate.delete(userId + ":syn"); 
  27.     logger.info(Thread.currentThread().getName() + ":釋放鎖-----"); 
  28.    } 
  29.    //關閉守護線程 
  30.    if (daemonThread != null) { 
  31.     daemonThread.stop(); 
  32.    } 
  33.   } 
  34.  } 

其中在獲得鎖之后,開啟守護線程,在finally中將守護線程關閉。

基于Lua腳本的實現

在上述邏輯中,我們是基于spring-boot-data-redis提供的原子化操作來保證鎖判斷和執行的原子化的。在非Spring Boot項目中,則可以基于Lua腳本來實現。

首先定義加鎖和解鎖的Lua腳本及對應的DefaultRedisScript對象,在RedisConfig配置類中添加如下實例化代碼:

  1. @Configuration 
  2. public class RedisConfig { 
  3.  
  4.  //lock script 
  5.  private static final String LOCK_SCRIPT = " if redis.call('setnx',KEYS[1],ARGV[1]) == 1 " + 
  6.    " then redis.call('expire',KEYS[1],ARGV[2]) " + 
  7.    " return 1 " + 
  8.    " else return 0 end "
  9.  private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call" + 
  10.    "('del', KEYS[1]) else return 0 end"
  11.  
  12.  // ... 省略部分代碼 
  13.   
  14.  @Bean 
  15.  public DefaultRedisScript<Boolean> lockRedisScript() { 
  16.   DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>(); 
  17.   defaultRedisScript.setResultType(Boolean.class); 
  18.   defaultRedisScript.setScriptText(LOCK_SCRIPT); 
  19.   return defaultRedisScript; 
  20.  } 
  21.  
  22.  @Bean 
  23.  public DefaultRedisScript<Long> unlockRedisScript() { 
  24.   DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>(); 
  25.   defaultRedisScript.setResultType(Long.class); 
  26.   defaultRedisScript.setScriptText(UNLOCK_SCRIPT); 
  27.   return defaultRedisScript; 
  28.  } 

再通過在AccountOperationThread類中新建構造方法,將上述兩個對象傳入類中(省略此部分演示)。然后,就可以基于RedisTemplate來調用了,改造之后的代碼實現如下:

  1. private void deamonRedisLockWithLua() { 
  2.  //守護線程 
  3.  DaemonThread daemonThread = null
  4.  //Spring data redis 支持的原子性操作,并設置5秒過期時間 
  5.  String uuid = UUID.randomUUID().toString(); 
  6.  String value = Thread.currentThread().getId() + ":" + uuid; 
  7.  try { 
  8.   while (!redisTemplate.execute(lockRedisScript, Collections.singletonList(userId + ":syn"), value, 5)) { 
  9.    // 等待1000毫秒重試獲得鎖 
  10.    logger.info(Thread.currentThread().getName() + ":嘗試循環獲取鎖"); 
  11.    TimeUnit.MILLISECONDS.sleep(1000); 
  12.   } 
  13.   logger.info(Thread.currentThread().getName() + ":獲得鎖----"); 
  14.   // 開啟守護線程 
  15.   daemonThread = new DaemonThread(userId + ":syn", redisTemplate); 
  16.   Thread thread = new Thread(daemonThread); 
  17.   thread.start(); 
  18.   // 業務邏輯執行10秒... 
  19.   TimeUnit.MILLISECONDS.sleep(10000); 
  20.  } catch (InterruptedException e) { 
  21.   logger.error("異常", e); 
  22.  } finally { 
  23.   //使用Lua腳本:先判斷是否是自己設置的鎖,再執行刪除 
  24.   // key存在,當前值=期望值時,刪除key;key存在,當前值!=期望值時,返回0; 
  25.   Long result = redisTemplate.execute(unlockRedisScript, Collections.singletonList(userId + ":syn"), value); 
  26.   logger.info("redis解鎖:{}", RELEASE_SUCCESS.equals(result)); 
  27.   if (RELEASE_SUCCESS.equals(result)) { 
  28.    if (daemonThread != null) { 
  29.     //關閉守護線程 
  30.     daemonThread.stop(); 
  31.     logger.info(Thread.currentThread().getName() + ":釋放鎖---"); 
  32.    } 
  33.   } 
  34.  } 

其中while循環中加鎖和finally中的釋放鎖都是基于Lua腳本來實現了。

Redis鎖的其他因素

除了上述實例,在使用Redis分布式鎖時,還可以考慮以下情況及方案。

Redis鎖的不可重入

當線程在持有鎖的情況下再次請求加鎖,如果一個鎖支持一個線程多次加鎖,那么這個鎖就是可重入的。如果一個不可重入鎖被再次加鎖,由于該鎖已經被持有,再次加鎖會失敗。Redis可通過對鎖進行重入計數,加鎖時加 1,解鎖時減 1,當計數歸 0時釋放鎖。

可重入鎖雖然高效但會增加代碼的復雜性,這里就不舉例說明了。

等待鎖釋放

有的業務場景,發現被鎖則直接返回。但有的場景下,客戶端需要等待鎖釋放然后去搶鎖。上述示例就屬于后者。針對等待鎖釋放也有兩種方案:

客戶端輪訓:當未獲得鎖時,等待一段時間再重新獲取,直到成功。上述示例就是基于這種方式實現的。這種方式的缺點也很明顯,比較耗費服務器資源,當并發量大時會影響服務器的效率。

使用Redis的訂閱發布功能:當獲取鎖失敗時,訂閱鎖釋放消息,獲取鎖成功后釋放時,發送釋放消息。

集群中的主備切換和腦裂

在Redis包含主從同步的集群部署方式中,如果主節點掛掉,從節點提升為主節點。如果客戶端A在主節點加鎖成功,指令還未同步到從節點,此時主節點掛掉,從節點升為主節點,新的主節點中沒有鎖的數據。這種情況下,客戶端B就可能加鎖成功,從而出現并發的場景。

當集群發生腦裂時,Redis master節點跟slave 節點和 sentinel 集群處于不同的網絡分區。sentinel集群無法感知到master的存在,會將 slave 節點提升為 master 節點,此時就會存在兩個不同的 master 節點。從而也會導致并發問題的出現。Redis Cluster集群部署方式同理。

小結

通過生產環境中的一個問題,排查原因,尋找解決方案,到最終對基于Redis分布式的深入研究,這便是學習的過程。

同時,每當面試或被問題如何解決分布式共享資源時,我們會脫口而出”基于Redis實現分布式鎖“,但通過本文的學習會發現,Redis分布式鎖并不是萬能的,而且在使用的過程中還需要注意超時、死鎖、誤解鎖、集群選主/腦裂等問題。

Redis以高性能著稱,但在實現分布式鎖的過程中還是存在一些問題。因此,基于Redis的分布式鎖可以極大的緩解并發問題,但要完全防止并發,還是得從數據庫層面入手。

源碼地址:https://github.com/secbr/springboot-all/tree/master/springboot-redis-lock

參考文章:

https://jinzhihong.github.io/2019/08/12/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-Redis-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0-%E4%B8%80/

https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/

 

 

責任編輯:武曉燕 來源: 程序新視界
相關推薦

2021-10-25 09:50:57

Redis分布式技術

2024-08-13 17:35:27

2020-11-16 12:55:41

Redis分布式鎖Zookeeper

2019-07-16 09:22:10

RedisZookeeper分布式鎖

2021-07-10 10:02:30

ZooKeeperCurator并發

2019-06-19 15:40:06

分布式鎖RedisJava

2021-06-30 14:56:12

Redisson分布式公平鎖

2022-05-26 10:27:41

分布式互聯網

2021-07-01 09:42:08

Redisson分布式

2022-10-10 14:41:44

RedisJVM數據

2022-03-07 08:14:27

并發分布式

2022-03-11 10:03:40

分布式鎖并發

2018-10-12 09:42:00

分布式鎖 Java多線

2021-06-27 21:24:55

RedissonJava數據

2022-01-06 10:58:07

Redis數據分布式鎖

2023-08-21 19:10:34

Redis分布式

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2021-12-01 10:13:48

場景分布式并發

2022-03-04 09:54:04

Redis分布式鎖腳本

2021-07-03 17:45:57

分布式Redisson MultiLock
點贊
收藏

51CTO技術棧公眾號

久久久久久久久久久电影| 亚洲影院一区| 欧美精品一区二区在线观看| 日本丰满少妇xxxx| 肉丝一区二区| 捆绑变态av一区二区三区| 色综合91久久精品中文字幕| 一区二区三区少妇| 97人人做人人爽香蕉精品| 亚洲欧美色图小说| 国产一区免费在线| 在线观看中文字幕2021| 黑人一区二区三区四区五区| 亚洲欧美制服综合另类| 人妻换人妻仑乱| 91福利在线尤物| 国产精品丝袜91| 国产欧美丝袜| 97人妻精品一区二区三区| 亚洲福利专区| 久久网福利资源网站| 久久久亚洲av波多野结衣| 精品国产伦一区二区三区观看说明 | 国产男男gay体育生网站| 日韩一级欧洲| 欧美人交a欧美精品| 人妻aⅴ无码一区二区三区| 成人福利一区| 欧美一区二区三区男人的天堂| 99999精品视频| 男人天堂亚洲| 亚洲天堂av老司机| 神马影院我不卡午夜| 国产成人三级在线播放| 美女国产一区二区三区| 欧美孕妇毛茸茸xxxx| 久久免费视频精品| 亚洲va在线| 深夜福利国产精品| 少妇久久久久久久久久| 日韩三区视频| 亚洲国产成人精品女人久久久| 黄色一级片免费播放| 久久不卡日韩美女| 精品视频一区二区三区免费| 久章草在线视频| 色网在线免费观看| 婷婷开心激情综合| 国产日韩av网站| heyzo高清在线| 亚洲国产一二三| 成人在线观看毛片| 日韩特级毛片| 亚洲一区二区在线播放相泽| 日产精品久久久久久久蜜臀| 亚洲资源一区| 亚洲精品va在线观看| 可以免费看的黄色网址| 操你啦视频在线| 亚洲你懂的在线视频| 穿情趣内衣被c到高潮视频| 黄在线免费看| 一区二区三区在线观看视频 | 国精产品一区一区三区免费视频 | 国产精品原创巨作av| 成人精品视频99在线观看免费| 在线观看中文字幕网站| 激情五月播播久久久精品| 成人性教育视频在线观看| 97人妻精品一区二区三区软件| 韩国毛片一区二区三区| 91福利入口| 欧美熟妇乱码在线一区| 不卡一区中文字幕| 欧美福利一区二区三区| 成年人免费在线视频| 国产精品久久久久aaaa樱花 | 99精品在线直播| 国精产品一品二品国精品69xx| av网站免费线看精品| 美媛馆国产精品一区二区| 成人精品一区| 一二三区精品福利视频| 久久综合久久久久| 制服丝袜专区在线| 欧美精品777| www.男人天堂| 国产亚洲一卡2卡3卡4卡新区 | 国内国产精品久久| 国产精品日韩二区| 黑人与亚洲人色ⅹvideos | 日韩免费电影一区二区| 黄页视频在线播放| 精品国产福利在线| 污污网站免费观看| 成人av影音| 一区二区亚洲精品国产| 欧美黄片一区二区三区| 麻豆成人在线| 91传媒视频在线观看| 天堂av网在线| 亚洲免费在线看| 国产精品69页| 7m精品国产导航在线| 亚洲图片在线综合| 久久婷婷国产麻豆91| 日韩av电影天堂| 国产伦精品一区二区三区视频免费| 男人的天堂在线视频| 亚洲欧美一区二区不卡| 国产精品97在线| 午夜视频在线观看精品中文| 亚洲天堂网站在线观看视频| 国产在线观看99| 精品亚洲国产成人av制服丝袜 | 日韩一区二区在线| 97香蕉久久夜色精品国产| 91国内精品久久久| 久久精品综合网| 久久久久久久久久网| 精品一区二区三区中文字幕 | 在线观看电影av| 在线日韩一区二区| 国产熟女高潮一区二区三区| 正在播放日韩欧美一页| 国产精品中文字幕在线观看| 深夜影院在线观看| 亚洲国产视频一区二区| 中文字幕第一页在线视频| 国产永久精品大片wwwapp| 97人洗澡人人免费公开视频碰碰碰| 一级二级三级视频| 国产欧美日韩在线观看| jizzjizzxxxx| 久久夜色精品国产噜噜av小说| 欧美精品亚州精品| 亚洲香蕉在线视频| 中文字幕高清不卡| 国产三区在线视频| 亚洲va久久| 欧美亚洲另类在线| 视频国产在线观看| 欧美色videos| wwwwxxxx国产| 午夜一级在线看亚洲| 狠狠色狠狠色综合人人| 草草在线视频| 亚洲二区中文字幕| 日本va欧美va国产激情| 99视频在线观看一区三区| 欧美久久久久久久久久久久久久| 国内精品视频| 欧美老肥婆性猛交视频| www香蕉视频| 亚洲影院在线观看| 欧美图片自拍偷拍| 亚洲国产免费看| 久久精品ww人人做人人爽| 午夜久久中文| 亚洲小视频在线| 天天干天天插天天射| 国产精品视频麻豆| www.cao超碰| 欧美99在线视频观看| 99久久一区三区四区免费| 欧美性受ⅹ╳╳╳黑人a性爽| 精品国产在天天线2019| 国产手机在线视频| 国产亚洲一区二区三区在线观看| 亚洲少妇久久久| 一区二区三区四区日韩| 国产精品免费观看高清| 天堂中文最新版在线中文| 亚洲色图色老头| 91精品视频免费在线观看| 一区二区三区色| 黄色a一级视频| 蜜臀a∨国产成人精品| 久久久久久久久久久久久国产| 成人午夜大片| 国产精品aaa| 超碰人人在线| 亚洲免费高清视频| 91theporn国产在线观看| 亚洲成人精品影院| 亚洲ⅴ国产v天堂a无码二区| 国产剧情av麻豆香蕉精品| 国产精彩视频一区二区| 国产一区毛片| 99在线视频免费观看| 日韩伦理三区| 免费91在线视频| 欧洲亚洲在线| 欧美一区二区在线免费观看| 日韩精品无码一区二区| 国产精品久久免费看| 曰本三级日本三级日本三级| 鲁大师影院一区二区三区| 黄色一级片网址| 久久91麻豆精品一区| 91免费版黄色| 欧美三区四区| 久久久久久亚洲| 欧美猛烈性xbxbxbxb| 日韩精品电影网| 国产女人爽到高潮a毛片| 色综合咪咪久久| 欧美黑人精品一区二区不卡| 国产精品日韩精品欧美在线| 日本黄色动态图| 国产在线国偷精品免费看| 午夜精品久久久内射近拍高清 | 日韩成人在线看| 久久久这里只有精品视频| 黑人与亚洲人色ⅹvideos| 亚洲国产精品美女| 国产免费不卡视频| 欧美日韩一区中文字幕| 可以免费在线观看的av| 亚洲男人天堂一区| av资源在线免费观看| 久久久美女毛片| 日韩 中文字幕| 成人一级视频在线观看| 黄色a级三级三级三级| 蜜臀av性久久久久av蜜臀妖精| 亚洲午夜无码av毛片久久| 亚洲毛片一区| 久久久久久久9| 欧美韩日精品| 亚洲自拍偷拍一区二区三区| 成人在线亚洲| 日本不卡久久| 亚洲精品国产动漫| 好看的日韩精品视频在线| 国产厕拍一区| 国产精品一区二区欧美黑人喷潮水| 国产精久久一区二区| 成人女保姆的销魂服务| 精品三级在线| 国产剧情日韩欧美| 欧洲亚洲精品久久久久| 国产精品视频自在线| 欧美影视资讯| 国产女同一区二区| 国产精品诱惑| 成人看片人aa| 精品国产乱码久久久久久樱花| 91免费高清视频| 精品一区91| 高清国语自产拍免费一区二区三区| 亚洲精品v亚洲精品v日韩精品| 91在线免费观看网站| 久久国产精品免费一区二区三区| 成人综合网网址| 亚洲精选av| 国产日韩欧美亚洲一区| 欧美男男freegayvideosroom| 久久国产精品高清| 亚洲人成伊人成综合图片| 欧美一区1区三区3区公司 | 一区二区三区亚洲视频| 欧美精品1区2区| www国产一区| 日韩av网站电影| 国产日本在线| 久久久精品在线观看| 黄色影院在线看| 欧洲一区二区视频| av成人免费看| 98国产高清一区| 日韩影视在线观看| 午夜精品一区二区在线观看| 亚洲人成免费网站| 人人干视频在线| 日韩电影免费在线看| 午夜免费福利网站| av在线综合网| 免费看的黄色录像| 亚洲黄色av一区| 亚洲av中文无码乱人伦在线视色| 欧美中文字幕亚洲一区二区va在线 | 国产福利在线播放麻豆| 欧美二区乱c黑人| 欧美91看片特黄aaaa| 91精品久久久久久久久久久久久久| 日本一区二区三区电影免费观看| 久久国产精品一区二区三区| 成人区精品一区二区婷婷| 国产一级片91| 日本伊人精品一区二区三区观看方式| 九九九九九伊人| av在线播放一区二区三区| 极品久久久久久久| 亚洲高清免费一级二级三级| 中文字幕 日韩有码| 亚洲精品在线观看网站| 在线免费观看的av网站| 91禁外国网站| 国产精品国产三级在线观看| 久久偷看各类wc女厕嘘嘘偷窃 | 国产欧美一区二区精品久导航| 亚洲国产精品免费在线观看| 色狠狠色狠狠综合| 日本高清视频免费看| 日韩小视频网址| 另类图片综合电影| 国产成人免费电影| 91欧美国产| 国产精品亚洲a| 成人h精品动漫一区二区三区| 黄色免费一级视频| 一本色道久久综合亚洲91 | 成人在线视频一区| 日韩精品久久久久久久的张开腿让| 亚洲国产美女搞黄色| 国产又大又长又粗| 中文字幕自拍vr一区二区三区| а√天堂8资源在线| 92福利视频午夜1000合集在线观看 | 日韩欧美国产精品| 日韩黄色影院| caoporn国产精品免费公开| 国产成人免费| 欧美二区在线| 国产视频一区免费看| 日本少妇激三级做爰在线| 国产精品久久影院| 天天天天天天天干| 亚洲精品在线看| 九色porny丨入口在线| 999视频在线免费观看| 久久精品av| 一道本视频在线观看| 久久久无码精品亚洲日韩按摩| 日韩美女一级片| 欧美精品一区二区在线观看| 国产白丝在线观看| 99在线视频播放| 在线视频观看日韩| 最新版天堂资源在线| 一区二区三区四区亚洲| 最新黄色网址在线观看| 国产一区二区激情| 欧美不卡高清一区二区三区| 欧美二区在线看| 日韩精品乱码免费| 亚洲色图第四色| 欧美日韩精品一区二区在线播放| 成年午夜在线| 国产一区在线播放| 五月开心六月丁香综合色啪 | 精品亚洲aⅴ在线观看| 国产剧情av在线播放| 精品91免费| 美女日韩在线中文字幕| 法国伦理少妇愉情| 色妹子一区二区| аⅴ资源新版在线天堂| 国产精品久久久久不卡| 色综合色综合| 亚洲免费在线播放视频| 一区二区三区四区在线播放| 日韩性xxxx| 热久久免费国产视频| 欧美色图激情小说| 操人视频免费看| 午夜精品久久久| 免费动漫网站在线观看| 国产精品一区二区三区久久久| 亚洲先锋影音| 秘密基地免费观看完整版中文| 欧美日韩亚洲天堂| 91在线视频| 国产精品日韩欧美一区二区三区 | 免费黄频在线观看| 一区二区三区四区中文字幕| 日本又骚又刺激的视频在线观看| 国产97在线|亚洲| 午夜精品视频一区二区三区在线看| 最新中文字幕日本| 91成人免费在线| www免费在线观看| 免费精品视频一区二区三区| 蜜桃av一区二区| 国产精品成人aaaa在线| 亚洲视频在线免费看| 精品一区二区三区免费看| 青青草原成人网| 亚洲色图19p| 女人天堂在线| 97se视频在线观看| 久久综合伊人| 欧美精品入口蜜桃| 国产一区二区三区在线| 51精品国产| 国产三级生活片| 日韩欧美一区二区在线| 伊人福利在线| 亚洲国产欧美日韩|