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