您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關如何使用redis分布式鎖解決高并發時的線程安全問題,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
package com.tiger.utils; public class TestMutilThread { // 總票量 public static int count = 10; public static void main(String[] args) { statrtMulti(); } public static void statrtMulti() { for (int i = 1; i <= 20; i++) { TicketRunnable tickrunner = new TicketRunnable(); Thread thread = new Thread(tickrunner, "Thread No: " + i); thread.start(); } } public static class TicketRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " start " + count); // TODO Auto-generated method stub // logger.info(Thread.currentThread().getName() // + " really start" + count); if (count <= 0) { System.out.println(Thread.currentThread().getName() + " ticket sold out ! No tickets remained!" + count); return; } else { count = count - 1; System.out.println(Thread.currentThread().getName() + " bought a ticket,now remaining :" + (count)); } } } }
測試結果,從結果可以看到,票數在不同的線程中已經出現混亂。
Thread No: 2 start 10 Thread No: 6 start 10 Thread No: 4 start 10 Thread No: 5 start 10 Thread No: 3 start 10 Thread No: 9 start 6 Thread No: 1 start 10 Thread No: 1 bought a ticket,now remaining :3 Thread No: 9 bought a ticket,now remaining :4 Thread No: 3 bought a ticket,now remaining :5 Thread No: 12 start 3 Thread No: 5 bought a ticket,now remaining :6 Thread No: 4 bought a ticket,now remaining :7 Thread No: 8 start 7 Thread No: 7 start 8 Thread No: 12 bought a ticket,now remaining :1 Thread No: 14 start 0 Thread No: 6 bought a ticket,now remaining :8 Thread No: 16 start 0 Thread No: 2 bought a ticket,now remaining :9 Thread No: 16 ticket sold out ! No tickets remained!0 Thread No: 14 ticket sold out ! No tickets remained!0 Thread No: 18 start 0 Thread No: 18 ticket sold out ! No tickets remained!0 Thread No: 7 bought a ticket,now remaining :0 Thread No: 15 start 0 Thread No: 8 bought a ticket,now remaining :1 Thread No: 13 start 2 Thread No: 19 start 0 Thread No: 11 start 3 Thread No: 11 ticket sold out ! No tickets remained!0 Thread No: 10 start 3 Thread No: 10 ticket sold out ! No tickets remained!0 Thread No: 19 ticket sold out ! No tickets remained!0 Thread No: 13 ticket sold out ! No tickets remained!0 Thread No: 20 start 0 Thread No: 20 ticket sold out ! No tickets remained!0 Thread No: 15 ticket sold out ! No tickets remained!0 Thread No: 17 start 0 Thread No: 17 ticket sold out ! No tickets remained!0
為了解決多線程時出現的混亂問題,這里給出真正的測試類!!!
真正的測試類,這里啟動20個線程,來搶10張票。
RedisTemplate 是用來實現redis操作的,由spring進行集成。這里是使用到了RedisTemplate,所以我以構造器的形式在外部將RedisTemplate傳入到測試類中。
MultiTestLock 是用來實現加鎖的工具類。
總票數使用volatile關鍵字,實現多線程時變量在系統內存中的可見性,這點可以去了解下volatile關鍵字的作用。
TicketRunnable用于模擬搶票功能。
其中由于lock與unlock之間存在if判斷,為保證線程安全,這里使用synchronized來保證。
測試類:
package com.tiger.utils; import java.io.Serializable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; public class MultiConsumer { Logger logger=LoggerFactory.getLogger(MultiTestLock.class); private RedisTemplate<Serializable, Serializable> redisTemplate; public MultiTestLock lock; //總票量 public volatile static int count = 10; public void statrtMulti() { lock = new MultiTestLock(redisTemplate); for (int i = 1; i <= 20; i++) { TicketRunnable tickrunner = new TicketRunnable(); Thread thread = new Thread(tickrunner, "Thread No: " + i); thread.start(); } } public class TicketRunnable implements Runnable { @Override public void run() { logger.info(Thread.currentThread().getName() + " start " + count); // TODO Auto-generated method stub if (count > 0) { // logger.info(Thread.currentThread().getName() // + " really start" + count); lock.lock(); synchronized (this) { if(count<=0){ logger.info(Thread.currentThread().getName() + " ticket sold out ! No tickets remained!" + count); lock.unlock(); return; }else{ count=count-1; logger.info(Thread.currentThread().getName() + " bought a ticket,now remaining :" + (count)); } } lock.unlock(); }else{ logger.info(Thread.currentThread().getName() + " ticket sold out !" + count); } } } public RedisTemplate<Serializable, Serializable> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate( RedisTemplate<Serializable, Serializable> redisTemplate) { this.redisTemplate = redisTemplate; } public MultiConsumer(RedisTemplate<Serializable, Serializable> redisTemplate) { super(); this.redisTemplate = redisTemplate; } }
Lock工具類:
我們知道為保證線程安全,程序中執行的操作必須時原子的。redis后續的版本中可以使用set key同時設置expire超時時間。
想起上次去 電信翼支付 面試時,面試官問過一個問題:分布式鎖如何防止死鎖,問題關鍵在于我們在分布式中進行加鎖操作時成功了,但是后續業務操作完畢執行解鎖時出現失敗。導致分布式鎖無法釋放。出現死鎖,后續的加鎖無法正常進行。所以這里設置expire超時時間的目的就是防止出現解鎖失敗的情況,這樣,即使解鎖失敗了,分布式鎖依然會在超時時間過了之后自動釋放。
具體在代碼中也有注釋,也可以作為參考。
package com.tiger.utils; import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import javax.sound.midi.MidiDevice.Info; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.SessionCallback; import org.springframework.data.redis.core.script.RedisScript; public class MultiTestLock implements Lock { Logger logger=LoggerFactory.getLogger(MultiTestLock.class); private RedisTemplate<Serializable, Serializable> redisTemplate; public MultiTestLock(RedisTemplate<Serializable, Serializable> redisTemplate) { super(); this.redisTemplate = redisTemplate; } @Override public void lock() { //這里使用while循環強制線程進來之后先進行搶鎖操作。只有搶到鎖才能進行后續操作 while(true){ if(tryLock()){ try { //這里讓線程睡500毫秒的目的是為了模擬業務耗時,確保業務結束時之前設置的值正好打到超時時間, //實際生產中可能有偏差,這里需要經驗 Thread.sleep(500l); // logger.info(Thread.currentThread().getName()+" time to awake"); return; } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }else{ try { //這里設置一個隨機毫秒的sleep目的時降低while循環的頻率 Thread.sleep(new Random().nextInt(200)+100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Override public boolean tryLock() { //這里也可以選用transactionSupport支持事務操作 SessionCallback<Object> sessionCallback=new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.opsForValue().setIfAbsent("secret", "answer"); //設置超時時間要根據業務實際的可能處理時間來,是一個經驗值 operations.expire("secret", 500l, TimeUnit.MILLISECONDS); Object object=operations.exec(); return object; } }; //執行兩部操作,這里會拿到一個數組值 [true,true],分別對應上述兩部操作的結果,如果中途出現第一次為false則表明第一步set值出錯 List<Boolean> result=(List) redisTemplate.execute(sessionCallback); // logger.info(Thread.currentThread().getName()+" try lock "+ result); if(true==result.get(0)||"true".equals(result.get(0)+"")){ logger.info(Thread.currentThread().getName()+" try lock success"); return true; }else{ return false; } } @Override public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException { // TODO Auto-generated method stub return false; } @Override public void unlock() { //unlock操作直接刪除鎖,如果執行完還沒有達到超時時間則直接刪除,讓后續的線程進行繼續操作。起到補刀的作用,確保鎖已經超時或被刪除 SessionCallback<Object> sessionCallback=new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.delete("secret"); Object object=operations.exec(); return object; } }; Object result=redisTemplate.execute(sessionCallback); } @Override public void lockInterruptibly() throws InterruptedException { // TODO Auto-generated method stub } @Override public Condition newCondition() { // TODO Auto-generated method stub return null; } public RedisTemplate<Serializable, Serializable> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate( RedisTemplate<Serializable, Serializable> redisTemplate) { this.redisTemplate = redisTemplate; } }
執行結果
可以看到,票數穩步減少,后續沒有搶到鎖的線程余票為0,無票可搶。
tips:
這其中也出現了一個問題,redis進行多部封裝操作時,系統報錯:ERR EXEC without MULTI
后經過查閱發現問題出在:
在spring中,多次執行MULTI命令不會報錯,因為第一次執行時,會將其內部的一個isInMulti變量設為true,后續每次執行命令是都會檢查這個變量,如果為true,則不執行命令。
而多次執行EXEC命令則會報開頭說的"ERR EXEC without MULTI"錯誤。
以上就是如何使用redis分布式鎖解決高并發時的線程安全問題,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。