一.模擬問題
最近在公司遇到一個問題,掛號系統(tǒng)是做的集群,比如啟動了兩個相同的服務(wù),病人掛號的時候可能會出現(xiàn)同號的情況,比如兩個病人掛出來的號都是上午2號.這就出現(xiàn)了問題,由于是集群部署的,所以單純在代碼中的方法中加鎖是不能解決這種情況的.下面我將模擬這種情況,用redis做分布式鎖來解決這個問題.
1.新建掛號明細(xì)表
2.在idea上新建項目
下圖是創(chuàng)建好的項目結(jié)構(gòu),上面那個parent項目是其他項目不用管它,和新建的沒有關(guān)系
3.開始創(chuàng)建controller,service,dao(mapper),寫好后整體結(jié)構(gòu)如下
這里貼上service實(shí)現(xiàn)類的代碼,主要代碼就是這一塊:
package com.zk.service.impl; import com.zk.mapper.MzMapper; import com.zk.service.MzService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; /** * 門診操作service實(shí)現(xiàn)類 * * @author zk * @date 2020-9-9 */ @Service public class MzServiceImpl implements MzService { @Autowired private MzMapper mzMapper; @Override public Map<String, Object> gh(String ksdm, String ysdm,String brid) { Map<String,Object> resultMap = new HashMap<>(); int ghxh = 0; //獲取當(dāng)前的掛號序號 Map<String, Object> ghxhMap = mzMapper.getGhxh(ksdm,ysdm); //如果為空,說明還沒有人掛這個醫(yī)生的號,當(dāng)前是一號 if(ghxhMap == null){ ghxh = 1; }else{ ghxh = (int)ghxhMap.get("GHXH"); ghxh++; } //實(shí)際場景中,先獲取到ghxh后,還會進(jìn)行收費(fèi)等其他操作,這里模擬一下需要耗費(fèi)時間,為了方便測試出現(xiàn)問題,這里時間設(shè)置稍微長一點(diǎn) try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //新增掛號明細(xì)記錄 mzMapper.addGhmx(ksdm,ysdm,ghxh,brid); resultMap.put("code","200"); resultMap.put("msg","success"); return resultMap; } }
4.進(jìn)行測試
1)清空數(shù)據(jù)庫表
2)使用postman發(fā)送post請求,假設(shè)ksdm=1表示皮膚科,ysdm=1表示醫(yī)生華佗,brbh=1表示張三,現(xiàn)在張三去醫(yī)院掛皮膚科華佗醫(yī)生的號,收費(fèi)員就會操作系統(tǒng)調(diào)用上面寫的掛號接口.
調(diào)用成功后,看看數(shù)據(jù)庫里的數(shù)據(jù)
可以看到張三掛到了華佗醫(yī)生的第一個號,接著把請求參數(shù)的brbh改成2表示李四,李四也去掛華佗醫(yī)生的號
請求成功后查看數(shù)據(jù)庫
可以看到李四掛了華佗醫(yī)生的第二個號.現(xiàn)在就是正常的掛號,沒有出現(xiàn)問題.
3)postman開第二個請求窗口,兩個窗口同時去掉接口進(jìn)行掛號
窗口一模擬張三掛號
窗口二模擬李四掛號
操作成功后看看數(shù)據(jù)庫
結(jié)果是張三和李四都掛到了三號,這就出現(xiàn)了線程安全問題.
3)使用加鎖的方式解決問題
方法加上鎖之后,測試確實(shí)沒有問題了,但是實(shí)際情況是集群部署的,并不是只有一個服務(wù)
4)啟動兩個服務(wù)
idea中設(shè)置允許啟動多個實(shí)例
修改端口號,第一個啟動的端口號是8080,這里改成8081
5)啟動兩個服務(wù)后再用postman去請求,張三請求8080服務(wù)接口
李四請求8081接口
兩個窗口同時請求的時候,再次出現(xiàn)了ghxh相同的情況,在方法上加同步鎖不能解決這個問題.
二.使用redis做分布式鎖解決問題
1.首先需要啟動一個redis,如果不知道redis怎么安裝使用的,可以看我之前寫的"redis的安裝和使用".因為之前我在虛擬機(jī)上安裝過了redis,這里直接啟動一個單節(jié)點(diǎn)redis
2.項目的pom.xml文件引入redis依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
3.在application.yml中配置redis
spring: redis: host: 192.168.1.6 port: 6379
4.新建redis鎖操作類
package com.zk.util; import org.apache.commons.lang3.StringUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * redis鎖操作類 * * @author zk * @date 2020-9-10 */ @Repository public class RedisLock { private StringRedisTemplate stringredisTemplate; public RedisLock(StringRedisTemplate stringredisTemplate) { this.stringredisTemplate = stringredisTemplate; } /** * 加鎖,無阻塞 * 加鎖過程必須設(shè)置過期時間 * 如果沒有設(shè)置過期時間,手動釋放鎖的操作出現(xiàn)問題,那么就發(fā)生死鎖,鎖永遠(yuǎn)不能被釋放. * 加鎖和設(shè)置過期時間過程必須是原子操作 * 如果加鎖后服務(wù)宕機(jī)或程序崩潰,來不及設(shè)置過期時間,同樣會發(fā)生死鎖. * * @param key 鎖id * @param expire 過期時間 * @return */ public String tryLock(String key, long expire) { String token = UUID.randomUUID().toString(); //setIfAbsent方法:當(dāng)key不存在的時候,設(shè)置成功并返回true,當(dāng)key存在的時候,設(shè)置失敗并返回false //token是對應(yīng)的value,expire是緩存過期時間 Boolean isSuccess = stringredisTemplate.opsForValue().setIfAbsent(key, token, expire, TimeUnit.MILLISECONDS); if (isSuccess) { return token; } return null; } /** * 加鎖,有阻塞 * * @param name 鎖名稱 * @param expire 鎖過期時間 * @param timeout 請求超時時間 * @return */ public String lock(String name, long expire, long timeout) { long startTime = System.currentTimeMillis(); String token; do { token = tryLock(name, expire); if (token == null) { if ((System.currentTimeMillis() - startTime) > (timeout - 50)) { break; } try { //try 50 per sec Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); return null; } } } while (token == null); return token; } /** * 解鎖操作 * 解鎖必須是解除自己加上的鎖 * 試想一個這樣的場景,服務(wù)A加鎖,但執(zhí)行效率非常慢,導(dǎo)致鎖失效后還未執(zhí)行完,但這時候服務(wù)B已經(jīng)拿到鎖了,這時候服務(wù)A執(zhí)行完畢了去解鎖, * 把服務(wù)B的鎖給解掉了,其他服務(wù)C、D、E...都可以拿到鎖了,這就有問題了. * 加鎖的時候我們可以設(shè)置唯一value,解鎖時判斷是不是自己先前的value就行了. * * @param key * @param token * @return */ public boolean unlock(String key, String token) { //解鎖時需要先取出key對應(yīng)的value進(jìn)行判斷是否相等,這也是為什么加鎖的時候需要放不重復(fù)的值作為value String value = stringredisTemplate.opsForValue().get("name"); if (StringUtils.equals(value, token)) { stringredisTemplate.delete(key); return true; } return false; } }
5.修改業(yè)務(wù)操作類,用上RedisLock
package com.zk.service.impl; import com.zk.mapper.MzMapper; import com.zk.service.MzService; import com.zk.util.RedisLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; /** * 門診操作service實(shí)現(xiàn)類 * * @author zk * @date 2020-9-9 */ @Service public class MzServiceImpl implements MzService { @Autowired private MzMapper mzMapper; @Autowired private RedisLock redisLock; @Override public Map<String, Object> gh(String ksdm, String ysdm, String brid) { Map<String, Object> resultMap = new HashMap<>(); int ghxh = 0; //加鎖操作 String token = null; token = redisLock.lock("gh", 3000,3500); try { //獲取到了鎖,執(zhí)行正常業(yè)務(wù) if (token != null) { //獲取當(dāng)前的掛號序號 Map<String, Object> ghxhMap = mzMapper.getGhxh(ksdm, ysdm); //如果為空,說明還沒有人掛這個醫(yī)生的號,當(dāng)前是一號 if (ghxhMap == null) { ghxh = 1; } else { ghxh = (int) ghxhMap.get("GHXH"); ghxh++; } //實(shí)際場景中,先獲取到ghxh后,還會進(jìn)行收費(fèi)等其他操作,這里模擬一下需要耗費(fèi)時間,為了方便測試出現(xiàn)問題,這里時間設(shè)置稍微長一點(diǎn) try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //新增掛號明細(xì)記錄 mzMapper.addGhmx(ksdm, ysdm, ghxh, brid); } else { resultMap.put("code", "401"); resultMap.put("msg", "其他窗口正在操作,請稍后再試"); return resultMap; } } finally { //解鎖 if (token != null) { boolean gh = redisLock.unlock("gh", token); } } resultMap.put("code", "200"); resultMap.put("msg", "success"); return resultMap; } }
6.再用postman開兩個窗口去請求,和上面的操作一樣,然后再看數(shù)據(jù)庫的數(shù)據(jù)
問題解決,不會再出現(xiàn)重復(fù)的ghxh.
總結(jié)
到此這篇關(guān)于SpringBoot中使用redis做分布式鎖的方法的文章就介紹到這了,更多相關(guān)SpringBoot redis分布式鎖內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://blog.csdn.net/hchvhg/article/details/108511239