1. 程式人生 > >Redis之分散式鎖

Redis之分散式鎖

  一、加鎖原因
  
  二、原子操作
  
  三、分散式鎖
  
  四、分散式鎖常見問題
  
  一、加鎖原因
  
  在一些比較高併發的業務場景,經常聽到通過加鎖的方法實現執行緒安全。
  
  下面簡單介紹一下
  
  1.1 加鎖方式
  
  資料庫鎖
  
  資料庫本身提供了鎖機制,比如樂觀鎖、悲觀鎖等等。下面給出我之前寫的一篇部落格,介紹一下mysql資料庫的鎖機制
  
  Mysql的鎖機制
  
  單體環境
  
  Java執行緒層面,Java的jdk本身就提供了,比如synchronized和ReentrantLock可重入鎖。這是實現單體環境鎖的一種方法,這裡簡單介紹一下,並不對synchronized和ReentrantLock進行詳細介紹。
  
  分散式環境
  
  上面介紹的都是單體環境的和資料庫層面的,下面介紹一下分散式環境的解決方法。分散式環境有兩種比較常用的解決方法,一種是通過Zookeeper實現分散式;一種是通過Redis實現分散式鎖。本部落格比較詳細地介紹一下redis分散式鎖,對於Zookeeper分散式鎖有時間再寫部落格介紹。
  
  1.2 業務場景
  
  為什麼加鎖?從業務來說其實就是有業務場景,技術層面是為了保證執行緒安全性,也就是說保證執行緒操作是原子操作。
  
  下面簡單介紹一下一些業務場景
  
  比如電商的秒殺場景,這就是一個高併發場景了,假如一個使用者購買了庫存只有10的8件商品,另外一個使用者也要購買5件商品,這兩個使用者是同時進行的,第一個使用者買了8件,庫存就只有2件了,第二個使用者再買5件,注意是和第一個使用者操作同時進行的,這時也是10-5,庫存只有5件了。假如第一個使用者搶佔了,庫存優先減8了,第一個使用者進行操作,同時進行,獲取到的庫存是10,這時就會出現業務問題了。
  
  二、原子操作
  
  原子操作定義
  
  部落格介紹一下原子操作,為什麼說到原子操作呢?貌似和分散式鎖不搭邊,其實不是的,我們說加鎖,其本質目的就是為了實現執行緒操作是原子性的,也就是原子操作。
  
  原子操作:是指不會被執行緒排程機制打斷的操作,而且期間不會有任何上下文切換(context switch)。
  
  2.1 context switch
  
  上面介紹一下上下文切換(context switch),上下文切換是計算機的cpu從一個任務,或者說程序,從一個任務(程序)切換到另外一個任務(程序),期間確保任務(程序)不衝突的過程。
  
  在國外的whatis.techtarget網站有進行了比較詳細的定義
  
  上下文切換定義
  
  三、分散式鎖
  
  3.1 實現方式
  
  可以實現方式 setnx+expire
  
  在Redis中實現分散式鎖,可以通過setnx和expire實現,setnx命令意思是set key if not exist,就是說已經有一個執行緒佔用了,就不執行set key操作
  
  語法:setnx key value;expire是設定key的時間
  
  這裡setnx key為tkey,value為tvalue
  
  >setnx tkey tvalue
  
  OK
  
  >get tkey
  
  tvalue
  
  >expire tkey 5
  
  OK
  
  >del tkey
  
  (integer) 1
  
  先setnx,然後再給key加一個時間5秒,5秒後自動釋放鎖。當然一個程序執行過程還沒5秒也可以就直接刪除key。那麼假如在setnx過程出現異常,鎖就不能釋放。
  
  為了避免上面所說的分散式鎖不能釋放問題,開源社群有很多分散式解決方案,很多第三方庫,直到redis2.8版本,作者給出了一個很好的解決方案。
  
  redis2.8版本對setnx命令和expire命令進行了拓展,使這兩個命令可以同時執行,也可以理解為同個事務了。
  
  語法:
  
  setnx key ex time nx
  
  例子,設定tkey,時間為5秒
  
  setnx tkey ex 5 nx
  
  四、分散式鎖常見問題
  
  4.1 超時問題
  
  假如在釋放鎖和另一個執行緒重新佔用鎖之間,執行時間過長,超過了鎖的超時設定,這時候就會出現,第一個執行緒的鎖已經被標記為過期了,可是在臨界區的執行程式還沒執行,也就是說鎖並沒有真正釋放。這時候如果第二個執行緒持有了鎖,就會出現臨界區的程式碼不能正常序列執行,因為第一個執行緒的鎖在臨界區還沒真正釋放。
  
  這是一種比較常見的Redis鎖不能釋放的超時問題。
  
  通過網上資料,有提供了一種解決方案思路,是通過在set key的時候給value值加一個時間戳字串或者一個特定的隨機數,比如uuid,可以表示特定執行緒的標識,這個標識要唯一。
  
  然後在刪除key,重新加鎖的時候,校驗這個value是否為第一個執行緒的,匹配正確才刪除key,這是一種方案,當然並不是很好的解決方案,只能說是相對安全的,因為在高併發情況下面,執行緒的呼叫機制還是可以支援另外的執行緒持有鎖的。
  
  4.2 叢集環境
  
  下面介紹一下叢集環境的鎖問題,業務場景,假如一個執行緒在主伺服器(master)釋放鎖的時候,master突然冗機了,也是就是說鎖還沒被釋放。這時叢集環境檢測到master機器冗機,就切換從伺服器(slave)作為主伺服器,這時候,另外一個執行緒進來了,佔有了同一個鎖,也就是出現了兩個執行緒同時佔有同一個鎖的情況了。通過keepalive和redis實現主從伺服器自動failover的方式或許可以解決問題。因為並沒有實踐過,所以不做詳細解釋。這篇部落格
  
  Redis自從自動failover或許可以參考。
  
  ReadLock演算法
  
  叢集環境的鎖同步是一個難題。上面的僅僅是我的想法並沒有實踐過,最近找到一個演算法可以解決,ReadLock演算法。readlock-py庫已經有對改演算法進行實踐。ReadLock演算法簡單原理就是通過先檢測set是否成功,set成功之後才向所有節點發送指令,釋放鎖。本部落格並不對ReadLock演算法做詳細介紹,有機會再寫部落格介紹。
  
  <dependency>
  
  <groupId>org.apache.tomcat.embed</groupId>
  
  <artifactId>tomcat-embed-jasper<www.thd540.com/ /artifactId>
  
  <scope>provided<www.dasheng178.com /scope>
  
  </dependency>
  
  <dependency>
  
  <groupId>javax.servlet</groupId>
  
  <artifactId>jstl</artifactId>
  
  </dependency>
  
  第四步:在SpringBoot的屬性檔案application.properties中配置JSP的路由
  
  spring.mvc.view.prefix=/
  
  spring.mvc.view.suffix=.jsp
  
  第五步:修改Maven的pom.xml檔案打包方式改成war(預設打包Jar,打包Jar包的方式使用Idea啟動是沒什麼問題,如果單獨執行Jar包就找不到JSP檔案,如果改成War包即可)
  
  <packaging>war</packaging>
  
  SpringBoot中使用Thymeleaf
  
  SpringBoot官方是推薦使用thymeleaf作為優選的檢視解析器,所以SpringBoot對Thymeleaf的支援非常好,這裡僅僅演示SpringBoot如何選用Thymeleaf作用預設檢視解析器。
  
  第一步:匯入Thymeleaf的依賴
  
  <dependency>
  
  <groupId>org.springframework.boot<www.quwanyule157.com /groupId>
  
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
  
  </dependency>
  
  第二步:建立存放Thymeleaf模板資料夾,在Resources目錄下建立templates目錄
  
  這個資料夾的名字可不是我麼隨便命名的啊,是SpringBoot在自動裝配Thymeleaf檢視解析器的時候就已經預定義好了,我們看一下它的定義原始碼。
  
  @ConfigurationProperties(prefix www.furggw.com= "spring.thymeleaf")
  
  public class ThymeleafProperties {
  
  private static final www.dasheng178.com Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
  
  public static final String DEFAULT_PREFIX = www.mcyllpt.com/"classpath:/templates/";
  
  public static final String DEFAULT_SUFFIX = ".html";
  
  }
  
  SpringBoot中使用Freemark
  
  第一步:匯入Maven依賴
  
  <dependency>
  
  <groupId>org.springframework.boot</groupId>
  
  <artifactId>spring-boot-starter-freemarker</artifactId>
  
  </dependency>
  
  第二步:建立存放Freemark模板資料夾,在Resources目錄下建立templates目錄
  
  @ConfigurationProperties(prefix www.feifanyule.cn/= "spring.freemarker")
  
  public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties {
  
  public static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";
  
  public static final String DEFAULT_www.wanchuang178.cn  PREFIX = "";
  
  public static final String DEFAULT_SUFFIX = ".ftl";
  
  }
  
  我們可以看到SpringBoot在自動裝配Freemarker檢視解析器預設是將模板檔案放在classpath:/templates/路徑內,我們同樣可以在SpringBoot的配置檔案中自行配置。
  
  小提示:我在寫Freemark檢視解析器的時候並沒有將第一個JSP內部資源解析器給刪除掉,所以他們是並存的,所以我們可以知道SpringBoot在裝配他們的時候給予設定了優先順序順序。從下圖可以看到他們的優先順序順序;Freemarker>Thymeleaf>InternalResourceViewResolver`