洞察探索如何利用兼容微信生态的小程序容器,实现跨平台开发,助力金融和车联网行业的数字化转型。
648
2023-04-13
SpringBoot中使用redis做分布式锁的方法
一.模拟问题
最近在公司遇到一个问题,挂号系统是做的集群,比如启动了两个相同的服务,病人挂号的时候可能会出现同号的情况,比如两个病人挂出来的号都是上午2号.这就出现了问题,由于是集群部署的,所以单纯在代码中的方法中加锁是不能解决这种情况的.下面我将模拟这种情况,用redis做分布式锁来解决这个问题.
1.新建挂号明细表
2.在idea上新建项目
下图是创建好的项目结构,上面那个parent项目是其他项目不用管它,和新建的没有关系
3.开始创建controller,service,dao(mapper),写好后整体结构如下
这里贴上service实现类的代码,主要代码就是这一块:
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实现类
*
* @author zk
* @date 2020-9-9
*/
@Service
public class MzServiceImpl implements MzService {
@Autowired
private MzMapper mzMapper;
@Override
public Map
Map
int ghxh = 0;
//获取当前的挂号序号
Map
//如果为空,说明还没有人挂这个医生的号,当前是一号
if(ghxhMap == null){
ghxh = 1;
}else{
ghxh = (int)ghxhMap.get("GHXH");
ghxh++;
}
//实际场景中,先获取到ghxh后,还会进行收费等其他操作,这里模拟一下需要耗费时间,为了方便测试出现问题,这里时间设置稍微长一点
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//新增挂号明细记录
mzMapper.addGhmx(ksdm,ysdm,ghxh,brid);
resultMap.put("code","200");
resultMap.put("msg","success");
return resultMap;
}
}
4.进行测试
1)清空数据库表
2)使用postman发送post请求,假设ksdm=1表示皮肤科,ysdm=1表示医生华佗,brbh=1表示张三,现在张三去医院挂皮肤科华佗医生的号,收费员就会操作系统调用上面写的挂号接口.
调用成功后,看看数据库里的数据
可以看到张三挂到了华佗医生的第一个号,接着把请求参数的brbh改成2表示李四,李四也去挂华佗医生的号
请求成功后查看数据库
可以看到李四挂了华佗医生的第二个号.现在就是正常的挂号,没有出现问题.
3)postman开第二个请求窗口,两个窗口同时去掉接口进行挂号
窗口一模拟张三挂号
窗口二模拟李四挂号
操作成功后看看数据库
结果是张三和李四都挂到了三号,这就出现了线程安全问题.
3)使用加锁的方式解决问题
方法加上锁之后,测试确实没有问题了,但是实际情况是集群部署的,并不是只有一个服务
4)启动两个服务
idea中设置允许启动多个实例
修改端口号,第一个启动的端口号是8080,这里改成8081
5)启动两个服务后再用postman去请求,张三请求8080服务接口
李四请求8081接口
两个窗口同时请求的时候,再次出现了ghxh相同的情况,在方法上加同步锁不能解决这个问题.
二.使用redis做分布式锁解决问题
1.首先需要启动一个redis,如果不知道redis怎么安装使用的,可以看我之前写的"redis的安装和使用".因为之前我在虚拟机上安装过了redis,这里直接启动一个单节点redis
2.项目的pom.xml文件引入redis依赖
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;
}
/**
* 加锁,无阻塞
* 加锁过程必须设置过期时间
* 如果没有设置过期时间,手动释放锁的操作出现问题,那么就发生死锁,锁永远不能被释放.
* 加锁和设置过期时间过程必须是原子操作
* 如果加锁后服务宕机或程序崩溃,来不及设置过期时间,同样会发生死锁.
*
* @param key 锁id
* @param expire 过期时间
* @return
*/
public String tryLock(String key, long expire) {
String token = UUID.randomUUID().toString();
//setIfAbsent方法:当key不存在的时候,设置成功并返回true,当key存在的时候,设置失败并返回false
//token是对应的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 - 5PYUbnhGer0)) {
break;
}
try {
//try 50 per sec
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
} while (token == null);
return token;
}
/**
* 解锁操作
* 解锁必须是解除自己加上的锁
* 试想一个这样的场景,服务A加锁,但执行效率非常慢,导致锁失效后还未执行完,但这时候服务B已经拿到锁了,这时候服务A执行完毕了去解锁,
* 把服务B的锁给解掉了,其他服务C、D、E...都可以拿到锁了,这就有问题了.
* 加锁的时候我们可以设置唯一value,解锁时判断是不是自己先前的value就行了.
*
* @param key
* @param token
* @return
*/
public boolean unlock(String key, String token) {
//解锁时需要先取出key对应的value进行判断是否相等,这也是为什么加锁的时候需要放不重复的值作为value
String value = stringredisTemplate.opsForValue().get("name");
if (StringUtils.equals(value, token)) {
stringredisTemplate.delete(key);
return true;
}
return false;
}
}
5.修改业务操作类,用上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实现类
*
* @author zk
* @date 2020-9-9
*/
@Service
public class MzServiceImpl implements MzService {
@Autowired
private MzMapper mzMapper;
@Autowired
private RedisLock redisLock;
@Override
public Map
Map
int ghxh = 0;
//加锁操作
String token = null;
token = redisLock.lock("gh", 3000,3500);
try {
//获取到了锁,执行正常业务
if (token != null) {
//获取当前的挂号序号
Map
//如果为空,说明还没有人挂这个医生的号,当前是一号
if (ghxhMap == null) {
ghxh = 1;
} else {
ghxh = (int) ghxhMap.get("GHXH");
ghxh++;
}
//实际场景中,先获取到ghxh后,还会进行收费等其他操作,这里模拟一下需要耗费时间,为了方便测试出现问题,这里时间设置稍微长一点
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//新增挂号明细记录
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开两个窗口去请求,和上面的操作一样,然后再看数据库的数据
问题解决,不会再出现重复的ghxh.
总结
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~