利用redis實(shí)現(xiàn)分布式鎖,快速解決高并發(fā)時(shí)的線程安全問(wèn)題
實(shí)際工作中,經(jīng)常會(huì)遇到多線程并發(fā)時(shí)的類似搶購(gòu)的功能,本篇描述一個(gè)簡(jiǎn)單的redis分布式鎖實(shí)現(xiàn)的多線程搶票功能。
直接上代碼。首先按照慣例,給出一個(gè)錯(cuò)誤的示范:
我們可以看看,當(dāng)20個(gè)線程一起來(lái)?yè)?0張票的時(shí)候,會(huì)發(fā)生什么事。
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)); } } } }
測(cè)試結(jié)果,從結(jié)果可以看到,票數(shù)在不同的線程中已經(jīng)出現(xiàn)混亂。
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
為了解決多線程時(shí)出現(xiàn)的混亂問(wèn)題,這里給出真正的測(cè)試類!!!
真正的測(cè)試類,這里啟動(dòng)20個(gè)線程,來(lái)?yè)?0張票。
RedisTemplate 是用來(lái)實(shí)現(xiàn)redis操作的,由spring進(jìn)行集成。這里是使用到了RedisTemplate,所以我以構(gòu)造器的形式在外部將RedisTemplate傳入到測(cè)試類中。
MultiTestLock 是用來(lái)實(shí)現(xiàn)加鎖的工具類。
總票數(shù)使用volatile關(guān)鍵字,實(shí)現(xiàn)多線程時(shí)變量在系統(tǒng)內(nèi)存中的可見(jiàn)性,這點(diǎn)可以去了解下volatile關(guān)鍵字的作用。
TicketRunnable用于模擬搶票功能。
其中由于lock與unlock之間存在if判斷,為保證線程安全,這里使用synchronized來(lái)保證。
測(cè)試類:
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工具類:
我們知道為保證線程安全,程序中執(zhí)行的操作必須時(shí)原子的。redis后續(xù)的版本中可以使用set key同時(shí)設(shè)置expire超時(shí)時(shí)間。
想起上次去 電信翼支付 面試時(shí),面試官問(wèn)過(guò)一個(gè)問(wèn)題:分布式鎖如何防止死鎖,問(wèn)題關(guān)鍵在于我們?cè)诜植际街羞M(jìn)行加鎖操作時(shí)成功了,但是后續(xù)業(yè)務(wù)操作完畢執(zhí)行解鎖時(shí)出現(xiàn)失敗。導(dǎo)致分布式鎖無(wú)法釋放。出現(xiàn)死鎖,后續(xù)的加鎖無(wú)法正常進(jìn)行。所以這里設(shè)置expire超時(shí)時(shí)間的目的就是防止出現(xiàn)解鎖失敗的情況,這樣,即使解鎖失敗了,分布式鎖依然會(huì)在超時(shí)時(shí)間過(guò)了之后自動(dòng)釋放。
具體在代碼中也有注釋,也可以作為參考。
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循環(huán)強(qiáng)制線程進(jìn)來(lái)之后先進(jìn)行搶鎖操作。只有搶到鎖才能進(jìn)行后續(xù)操作 while(true){ if(tryLock()){ try { //這里讓線程睡500毫秒的目的是為了模擬業(yè)務(wù)耗時(shí),確保業(yè)務(wù)結(jié)束時(shí)之前設(shè)置的值正好打到超時(shí)時(shí)間, //實(shí)際生產(chǎn)中可能有偏差,這里需要經(jīng)驗(yàn) Thread.sleep(500l); // logger.info(Thread.currentThread().getName()+" time to awake"); return; } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }else{ try { //這里設(shè)置一個(gè)隨機(jī)毫秒的sleep目的時(shí)降低while循環(huán)的頻率 Thread.sleep(new Random().nextInt(200)+100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Override public boolean tryLock() { //這里也可以選用transactionSupport支持事務(wù)操作 SessionCallback<Object> sessionCallback=new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.opsForValue().setIfAbsent("secret", "answer"); //設(shè)置超時(shí)時(shí)間要根據(jù)業(yè)務(wù)實(shí)際的可能處理時(shí)間來(lái),是一個(gè)經(jīng)驗(yàn)值 operations.expire("secret", 500l, TimeUnit.MILLISECONDS); Object object=operations.exec(); return object; } }; //執(zhí)行兩部操作,這里會(huì)拿到一個(gè)數(shù)組值 [true,true],分別對(duì)應(yīng)上述兩部操作的結(jié)果,如果中途出現(xiàn)第一次為false則表明第一步set值出錯(cuò) 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操作直接刪除鎖,如果執(zhí)行完還沒(méi)有達(dá)到超時(shí)時(shí)間則直接刪除,讓后續(xù)的線程進(jìn)行繼續(xù)操作。起到補(bǔ)刀的作用,確保鎖已經(jīng)超時(shí)或被刪除 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; } }
執(zhí)行結(jié)果
可以看到,票數(shù)穩(wěn)步減少,后續(xù)沒(méi)有搶到鎖的線程余票為0,無(wú)票可搶。
tips:
這其中也出現(xiàn)了一個(gè)問(wèn)題,redis進(jìn)行多部封裝操作時(shí),系統(tǒng)報(bào)錯(cuò):ERR EXEC without MULTI
后經(jīng)過(guò)查閱發(fā)現(xiàn)問(wèn)題出在:
在spring中,多次執(zhí)行MULTI命令不會(huì)報(bào)錯(cuò),因?yàn)榈谝淮螆?zhí)行時(shí),會(huì)將其內(nèi)部的一個(gè)isInMulti變量設(shè)為true,后續(xù)每次執(zhí)行命令是都會(huì)檢查這個(gè)變量,如果為true,則不執(zhí)行命令。
而多次執(zhí)行EXEC命令則會(huì)報(bào)開(kāi)頭說(shuō)的"ERR EXEC without MULTI"錯(cuò)誤。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
了解Redis常見(jiàn)應(yīng)用場(chǎng)景
Redis是一個(gè)key-value存儲(chǔ)系統(tǒng),現(xiàn)在在各種系統(tǒng)中的使用越來(lái)越多,大部分情況下是因?yàn)槠涓咝阅艿奶匦裕划?dāng)做緩存使用,這里介紹下Redis經(jīng)常遇到的使用場(chǎng)景2021-06-06CentOS系統(tǒng)中Redis數(shù)據(jù)庫(kù)的安裝配置指南
Redis是一個(gè)基于主存存儲(chǔ)的數(shù)據(jù)庫(kù),性能很強(qiáng),這里我們就來(lái)看一下CentOS系統(tǒng)中Redis數(shù)據(jù)庫(kù)的安裝配置指南,包括將Redis作為系統(tǒng)服務(wù)運(yùn)行的技巧等,需要的朋友可以參考下2016-06-06Redis有序集合類型的操作_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
今天通過(guò)本文給大家說(shuō)一下Redis中最后一個(gè)數(shù)據(jù)類型 “有序集合類型”,需要的的朋友參考下吧2017-08-08springboot +redis 實(shí)現(xiàn)點(diǎn)贊、瀏覽、收藏、評(píng)論等數(shù)量的增減操作
這篇文章主要介紹了springboot +redis 實(shí)現(xiàn)點(diǎn)贊、瀏覽、收藏、評(píng)論等數(shù)量的增減操作,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09redis+lua實(shí)現(xiàn)限流的項(xiàng)目實(shí)踐
redis有很多限流的算法(比如:令牌桶,計(jì)數(shù)器,時(shí)間窗口)等,在分布式里面進(jìn)行限流的話,我們則可以使用redis+lua腳本進(jìn)行限流,下面就來(lái)介紹一下redis+lua實(shí)現(xiàn)限流2023-10-10Spring boot+redis實(shí)現(xiàn)消息發(fā)布與訂閱的代碼
這篇文章主要介紹了Spring boot+redis實(shí)現(xiàn)消息發(fā)布與訂閱,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值需要的朋友可以參考下2020-04-04