日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
鎖的基本概念到Redis分布式鎖實現(xiàn)

鎖的基本概念到Redis分布式鎖實現(xiàn)

作者:楊亨 2019-12-25 14:35:33

云計算

分布式

Redis 近來,分布式的問題被廣泛提及,比如分布式事務、分布式框架、ZooKeeper、SpringCloud等等。

我們提供的服務有:成都網(wǎng)站制作、網(wǎng)站建設、微信公眾號開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認證、鎮(zhèn)賚ssl等。為近千家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務,是有科學管理、有技術的鎮(zhèn)賚網(wǎng)站制作公司

 近來,分布式的問題被廣泛提及,比如分布式事務、分布式框架、ZooKeeper、SpringCloud等等。本文先回顧鎖的概念,再介紹分布式鎖,以及如何用Redis來實現(xiàn)分布式鎖。

一、鎖的基本了解

首先,回顧一下我們工作學習中的鎖的概念。

為什么要先講鎖再講分布式鎖呢?

我們都清楚,鎖的作用是要解決多線程對共享資源的訪問而產(chǎn)生的線程安全問題,而在平時生活中用到鎖的情況其實并不多,可能有些朋友對鎖的概念和一些基本的使用不是很清楚,所以我們先看鎖,再深入介紹分布式鎖。

通過一個賣票的小案例來看,比如大家去搶dota2 ti9門票,如果不加鎖的話會出現(xiàn)什么問題?此時代碼如下:

  
 
 
 
  1. package Thread;
  2. import java.util.concurrent.TimeUnit;
  3. public class Ticket {
  4. /**
  5. * 初始庫存量
  6. * */
  7. Integer ticketNum = 8;
  8. public void reduce(int num){
  9. //判斷庫存是否夠用
  10. if((ticketNum - num) >= 0){
  11. try {
  12. TimeUnit.MILLISECONDS.sleep(200);
  13. }catch (InterruptedException e){
  14. e.printStackTrace();
  15. }
  16. ticketNum -= num;
  17. System.out.println(Thread.currentThread().getName() + "成功賣出"
  18. + num + "張,剩余" + ticketNum + "張票");
  19. }else {
  20. System.err.println(Thread.currentThread().getName() + "沒有賣出"
  21. + num + "張,剩余" + ticketNum + "張票");
  22. }
  23. }
  24. public static void main(String[] args) throws InterruptedException{
  25. Ticket ticket = new Ticket();
  26. //開啟10個線程進行搶票,按理說應該有兩個人搶不到票
  27. for(int i=0;i<10;i++){
  28. new Thread(() -> ticket.reduce(1),"用戶" + (i + 1)).start();
  29. }
  30. Thread.sleep(1000L);
  31. }
  32. }

代碼分析:這里有8張ti9門票,設置了10個線程(也就是模擬10個人)去并發(fā)搶票,如果搶成功了顯示成功,搶失敗的話顯示失敗。按理說應該有8個人搶成功了,2個人搶失敗,下面來看運行結果:

我們發(fā)現(xiàn)運行結果和預期的情況不一致,居然10個人都買到了票,也就是說出現(xiàn)了線程安全的問題,那么是什么原因導致的呢?

原因就是多個線程之間產(chǎn)生了時間差。

如圖所示,只剩一張票了,但是兩個線程都讀到的票余量是1,也就是說線程B還沒有等到線程A改庫存就已經(jīng)搶票成功了。

  
 
 
 
  1. 怎么解決呢?想必大家都知道,加個synchronized關鍵字就可以了,在一個線程進行reduce方法的時候,其他線程則阻塞在等待隊列中,這樣就不會發(fā)生多個線程對共享變量的競爭問題。 
  2. 舉個例子 
  3. 比如我們去健身房健身,如果好多人同時用一臺機器,同時在一臺跑步機上跑步,就會發(fā)生很大的問題,大家會打得不可開交。如果我們加一把鎖在健身房門口,只有拿到鎖的鑰匙的人才可以進去鍛煉,其他人在門外等候,這樣就可以避免大家對健身器材的競爭。代碼如下: 
  4. public  synchronized void reduce(int num){ 
  5.         //判斷庫存是否夠用 
  6.         if((ticketNum - num) >= 0){ 
  7.             try { 
  8.                 TimeUnit.MILLISECONDS.sleep(200); 
  9.             }catch (InterruptedException e){ 
  10.                 e.printStackTrace(); 
  11.             } 
  12.             ticketNum -= num; 
  13.             System.out.println(Thread.currentThread().getName() + "成功賣出" 
  14.             + num + "張,剩余" + ticketNum + "張票"); 
  15.         }else { 
  16.             System.err.println(Thread.currentThread().getName() + "沒有賣出" 
  17.                     + num + "張,剩余" + ticketNum + "張票"); 
  18.         } 
  19.     } 

運行結果:

果不其然,結果有兩個人沒有成功搶到票,看來我們的目的達成了。

二、鎖的性能優(yōu)化

2.1 縮短鎖的持有時間

事實上,按照我們對日常生活的理解,不可能整個健身房只有一個人在運動。所以我們只需要對某一臺機器加鎖就可以了,比如一個人在跑步,另一個人可以去做其他的運動。

對于票務系統(tǒng)來說,我們只需要對庫存的修改操作的代碼加鎖就可以了,別的代碼還是可以并行進行,這樣會大大減少鎖的持有時間,代碼修改如下:

  
 
 
 
  1. public void reduceByLock(int num){
  2. boolean flag = false;
  3. synchronized (ticketNum){
  4. if((ticketNum - num) >= 0){
  5. ticketNum -= num;
  6. flag = true;
  7. }
  8. }
  9. if(flag){
  10. System.out.println(Thread.currentThread().getName() + "成功賣出"
  11. + num + "張,剩余" + ticketNum + "張票");
  12. }
  13. else {
  14. System.err.println(Thread.currentThread().getName() + "沒有賣出"
  15. + num + "張,剩余" + ticketNum + "張票");
  16. }
  17. if(ticketNum == 0){
  18. System.out.println("耗時" + (System.currentTimeMillis() - startTime) + "毫秒");
  19. }
  20. }
  21. 這樣做的目的是充分利用cpu的資源,提高代碼的執(zhí)行效率。
  22. 這里我們對兩種方式的時間做個打?。?/li>
  23. public synchronized void reduce(int num){
  24. //判斷庫存是否夠用
  25. if((ticketNum - num) >= 0){
  26. try {
  27. TimeUnit.MILLISECONDS.sleep(200);
  28. }catch (InterruptedException e){
  29. e.printStackTrace();
  30. }
  31. ticketNum -= num;
  32. if(ticketNum == 0){
  33. System.out.println("耗時" + (System.currentTimeMillis() - startTime) + "毫秒");
  34. }
  35. System.out.println(Thread.currentThread().getName() + "成功賣出"
  36. + num + "張,剩余" + ticketNum + "張票");
  37. }else {
  38. System.err.println(Thread.currentThread().getName() + "沒有賣出"
  39. + num + "張,剩余" + ticketNum + "張票");
  40. }
  41. }

果然,只對部分代碼加鎖會大大提供代碼的執(zhí)行效率。

所以,在解決了線程安全的問題后,我們還要考慮到加鎖之后的代碼執(zhí)行效率問題。

2.2 減少鎖的粒度

舉個例子,有兩場電影,分別是最近剛上映的魔童哪吒和蜘蛛俠,我們模擬一個支付購買的過程,讓方法等待,加了一個CountDownLatch的await方法,運行結果如下:

  
 
 
 
  1. package Thread;
  2. import java.util.concurrent.CountDownLatch;
  3. public class Movie {
  4. private final CountDownLatch latch = new CountDownLatch(1);
  5. //魔童哪吒
  6. private Integer babyTickets = 20;
  7. //蜘蛛俠
  8. private Integer spiderTickets = 100;
  9. public synchronized void showBabyTickets() throws InterruptedException{
  10. System.out.println("魔童哪吒的剩余票數(shù)為:" + babyTickets);
  11. //購買
  12. latch.await();
  13. }
  14. public synchronized void showSpiderTickets() throws InterruptedException{
  15. System.out.println("蜘蛛俠的剩余票數(shù)為:" + spiderTickets);
  16. //購買
  17. }
  18. public static void main(String[] args) {
  19. Movie movie = new Movie();
  20. new Thread(() -> {
  21. try {
  22. movie.showBabyTickets();
  23. }catch (InterruptedException e){
  24. e.printStackTrace();
  25. }
  26. },"用戶A").start();
  27. new Thread(() -> {
  28. try {
  29. movie.showSpiderTickets();
  30. }catch (InterruptedException e){
  31. e.printStackTrace();
  32. }
  33. },"用戶B").start();
  34. }
  35. }

執(zhí)行結果:

魔童哪吒的剩余票數(shù)為:20

我們發(fā)現(xiàn)買哪吒票的時候阻塞會影響蜘蛛俠票的購買,而實際上,這兩場電影之間是相互獨立的,所以我們需要減少鎖的粒度,將movie整個對象的鎖變?yōu)閮蓚€全局變量的鎖,修改代碼如下:

  
 
 
 
  1. public void showBabyTickets() throws InterruptedException{
  2. synchronized (babyTickets) {
  3. System.out.println("魔童哪吒的剩余票數(shù)為:" + babyTickets);
  4. //購買
  5. latch.await();
  6. }
  7. }
  8. public void showSpiderTickets() throws InterruptedException{
  9. synchronized (spiderTickets) {
  10. System.out.println("蜘蛛俠的剩余票數(shù)為:" + spiderTickets);
  11. //購買
  12. }
  13. }

執(zhí)行結果:

魔童哪吒的剩余票數(shù)為:20

蜘蛛俠的剩余票數(shù)為:100

現(xiàn)在兩場電影的購票不會互相影響了,這就是第二個優(yōu)化鎖的方式:減少鎖的粒度。順便提一句,Java并發(fā)包里的ConcurrentHashMap就是把一把大鎖變成了16把小鎖,通過分段鎖的方式達到高效的并發(fā)安全。

2.3 鎖分離

鎖分離就是常說的讀寫分離,我們把鎖分成讀鎖和寫鎖,讀的鎖不需要阻塞,而寫的鎖要考慮并發(fā)問題。

三、鎖的種類

  • 公平鎖:ReentrantLock
  • 非公平鎖:Synchronized、ReentrantLock、cas
  • 悲觀鎖:Synchronized
  • 樂觀鎖:cas
  • 獨享鎖:Synchronized、ReentrantLock
  • 共享鎖:Semaphore

這里就不一一講述每一種鎖的概念了,大家可以自己學習,鎖還可以按照偏向鎖、輕量級鎖、重量級鎖來分類。

四、Redis分布式鎖

了解了鎖的基本概念和鎖的優(yōu)化后,重點介紹分布式鎖的概念。

上圖所示是我們搭建的分布式環(huán)境,有三個購票項目,對應一個庫存,每一個系統(tǒng)會有多個線程,和上文一樣,對庫存的修改操作加上鎖,能不能保證這6個線程的線程安全呢?

當然是不能的,因為每一個購票系統(tǒng)都有各自的JVM進程,互相獨立,所以加synchronized只能保證一個系統(tǒng)的線程安全,并不能保證分布式的線程安全。

所以需要對于三個系統(tǒng)都是公共的一個中間件來解決這個問題。

這里我們選擇Redis來作為分布式鎖,多個系統(tǒng)在Redis中set同一個key,只有key不存在的時候,才能設置成功,并且該key會對應其中一個系統(tǒng)的唯一標識,當該系統(tǒng)訪問資源結束后,將key刪除,則達到了釋放鎖的目的。

4.1 分布式鎖需要注意哪些點

1)互斥性

在任意時刻只有一個客戶端可以獲取鎖。

這個很容易理解,所有的系統(tǒng)中只能有一個系統(tǒng)持有鎖。

2)防死鎖

假如一個客戶端在持有鎖的時候崩潰了,沒有釋放鎖,那么別的客戶端無法獲得鎖,則會造成死鎖,所以要保證客戶端一定會釋放鎖。

Redis中我們可以設置鎖的過期時間來保證不會發(fā)生死鎖。

3)持鎖人解鎖

解鈴還須系鈴人,加鎖和解鎖必須是同一個客戶端,客戶端A的線程加的鎖必須是客戶端A的線程來解鎖,客戶端不能解開別的客戶端的鎖。

4)可重入

當一個客戶端獲取對象鎖之后,這個客戶端可以再次獲取這個對象上的鎖。

4.2 Redis分布式鎖流程

Redis分布式鎖的具體流程:

1)首先利用Redis緩存的性質在Redis中設置一個key-value形式的鍵值對,key就是鎖的名稱,然后客戶端的多個線程去競爭鎖,競爭成功的話將value設為客戶端的唯一標識。

2)競爭到鎖的客戶端要做兩件事:

  • 設置鎖的有效時間 目的是防死鎖 (非常關鍵)

需要根據(jù)業(yè)務需要,不斷的壓力測試來決定有效期的長短。

  • 分配客戶端的唯一標識,目的是保證持鎖人解鎖(非常重要)

所以這里的value就設置成唯一標識(比如uuid)。

3)訪問共享資源

4)釋放鎖,釋放鎖有兩種方式,第一種是有效期結束后自動釋放鎖,第二種是先根據(jù)唯一標識判斷自己是否有釋放鎖的權限,如果標識正確則釋放鎖。

4.3 加鎖和解鎖

4.3.1 加鎖

1)setnx命令加鎖

set if not exists 我們會用到Redis的命令setnx,setnx的含義就是只有鎖不存在的情況下才會設置成功。

2)設置鎖的有效時間,防止死鎖 expire

加鎖需要兩步操作,思考一下會有什么問題嗎?

假如我們加鎖完之后客戶端突然掛了呢?那么這個鎖就會成為一個沒有有效期的鎖,接著就可能發(fā)生死鎖。雖然這種情況發(fā)生的概率很小,但是一旦出現(xiàn)問題會很嚴重,所以我們也要把這兩步合為一步。

幸運的是,Redis3.0已經(jīng)把這兩個指令合在一起成為一個新的指令。

來看jedis的官方文檔中的源碼:

  
 
 
 
  1. public String set(String key, String value, String nxxx, String expx, long time) {
  2. this.checkIsInMultiOrPipeline();
  3. this.client.set(key, value, nxxx, expx, time);
  4. return this.client.getStatusCodeReply();
  5. }

這就是我們想要的!

4.3.2 解鎖

  1. 檢查是否自己持有鎖(判斷唯一標識);
  2. 刪除鎖。

解鎖也是兩步,同樣也要保證解鎖的原子性,把兩步合為一步。

這就無法借助于Redis了,只能依靠Lua腳本來實現(xiàn)。

  
 
 
 
  1. if Redis.call("get",key==argv[1])then
  2. return Redis.call("del",key)
  3. else return 0 end

這就是一段判斷是否自己持有鎖并釋放鎖的Lua腳本。

為什么Lua腳本是原子性呢?因為Lua腳本是jedis用eval()函數(shù)執(zhí)行的,如果執(zhí)行則會全部執(zhí)行完成。

五、Redis分布式鎖代碼實現(xiàn)

  
 
 
 
  1. public class RedisDistributedLock implements Lock {
  2. //上下文,保存當前鎖的持有人id
  3. private ThreadLocal lockContext = new ThreadLocal();
  4. //默認鎖的超時時間
  5. private long time = 100;
  6. //可重入性
  7. private Thread ownerThread;
  8. public RedisDistributedLock() {
  9. }
  10. public void lock() {
  11. while (!tryLock()){
  12. try {
  13. Thread.sleep(100);
  14. }catch (InterruptedException e){
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. public boolean tryLock() {
  20. return tryLock(time,TimeUnit.MILLISECONDS);
  21. }
  22. public boolean tryLock(long time, TimeUnit unit){
  23. String id = UUID.randomUUID().toString(); //每一個鎖的持有人都分配一個唯一的id
  24. Thread t = Thread.currentThread();
  25. Jedis jedis = new Jedis("127.0.0.1",6379);
  26. //只有鎖不存在的時候加鎖并設置鎖的有效時間
  27. if("OK".equals(jedis.set("lock",id, "NX", "PX", unit.toMillis(time)))){
  28. //持有鎖的人的id
  29. lockContext.set(id); ①
  30. //記錄當前的線程
  31. setOwnerThread(t); ②
  32. return true;
  33. }else if(ownerThread == t){
  34. //因為鎖是可重入的,所以需要判斷當前線程已經(jīng)持有鎖的情況
  35. return true;
  36. }else {
  37. return false;
  38. }
  39. }
  40. private void setOwnerThread(Thread t){
  41. this.ownerThread = t;
  42. }
  43. public void unlock() {
  44. String script = null;
  45. try{
  46. Jedis jedis = new Jedis("127.0.0.1",6379);
  47. script = inputStream2String(getClass().getResourceAsStream("/Redis.Lua"));
  48. if(lockContext.get()==null){
  49. //沒有人持有鎖
  50. return;
  51. }
  52. //刪除鎖 ③
  53. jedis.eval(script, Arrays.asList("lock"), Arrays.asList(lockContext.get()));
  54. lockContext.remove();
  55. }catch (Exception e){
  56. e.printStackTrace();
  57. }
  58. }
  59. /**
  60. * 將InputStream轉化成String
  61. * @param is
  62. * @return
  63. * @throws IOException
  64. */
  65. public String inputStream2String(InputStream is) throws IOException {
  66. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  67. int i = -1;
  68. while ((i = is.read()) != -1) {
  69. baos.write(i);
  70. }
  71. return baos.toString();
  72. }
  73. public void lockInterruptibly() throws InterruptedException {
  74. }
  75. public Condition newCondition() {
  76. return null;
  77. }
  78. }
  • 用一個上下文全局變量來記錄持有鎖的人的uuid,解鎖的時候需要將該uuid作為參數(shù)傳入Lua腳本中,來判斷是否可以解鎖。
  • 要記錄當前線程,來實現(xiàn)分布式鎖的重入性,如果是當前線程持有鎖的話,也屬于加鎖成功。
  • 用eval函數(shù)來執(zhí)行Lua腳本,保證解鎖時的原子性。

六、分布式鎖的對比

6.1 基于數(shù)據(jù)庫的分布式鎖

1)實現(xiàn)方式

獲取鎖的時候插入一條數(shù)據(jù),解鎖時刪除數(shù)據(jù)。

2)缺點

  • 數(shù)據(jù)庫如果掛掉會導致業(yè)務系統(tǒng)不可用。
  • 無法設置過期時間,會造成死鎖。

6.2 基于zookeeper的分布式鎖

1)實現(xiàn)方式

加鎖時在指定節(jié)點的目錄下創(chuàng)建一個新節(jié)點,釋放鎖的時候刪除這個臨時節(jié)點。因為有心跳檢測的存在,所以不會發(fā)生死鎖,更加安全。

2)缺點

性能一般,沒有Redis高效。

所以:

  • 從性能角度: Redis > zookeeper > 數(shù)據(jù)庫 
  • 從可靠性(安全)性角度: zookeeper > Redis > 數(shù)據(jù)庫

七、總結

本文從鎖的基本概念出發(fā),提出多線程訪問共享資源會出現(xiàn)的線程安全問題,然后通過加鎖的方式去解決線程安全的問題,這個方法會性能會下降,需要通過:縮短鎖的持有時間、減小鎖的粒度、鎖分離三種方式去優(yōu)化鎖。

之后介紹了分布式鎖的4個特點:

  • 互斥性
  • 防死鎖
  • 加鎖人解鎖
  • 可重入性

然后用Redis實現(xiàn)了分布式鎖,加鎖的時候用到了Redis的命令去加鎖,解鎖的時候則借助了Lua腳本來保證原子性。

最后對比了三種分布式鎖的優(yōu)缺點和使用場景。

希望大家對分布式鎖有新的理解,也希望大家在考慮解決問題的同時要多想想性能的問題。

【本文是51CTO專欄機構宜信技術學院的原創(chuàng)文章,微信公眾號“宜信技術學院( id: CE_TECH)”】


當前文章:鎖的基本概念到Redis分布式鎖實現(xiàn)
網(wǎng)頁路徑:http://www.dlmjj.cn/article/djececd.html