Java中的锁(单机锁、分布式锁)
JVM本地锁:
概念:
单机锁只存在于单体项目中,会存在并发问题(即同一时刻多个请求进来导致对数据库表数据的重复的CUD操作
,不用考虑多线程问题即集群问题
);
举例:账户默认有500块,一个线程循环5次存100,一个线程循环5次取100,最后结果应该是500不变。如果不处理的话再高并发情况下会出现结果超过500或者负数的情况(出现了幂等性/数据泄露问题)
;
解决方案:
- 在Service层的实现类的
方法前面加入JVM的synchronized关键字
,即可解决单体项目中的并发问题
/**
* 并发问题使用:本地锁
*/
@Override
// 方法前面加synchronized关键字
public synchronized R registerUser(UserRegisterVo userRegisterVo) {
String tel = userRegisterVo.getTel();
int i = userMapper.existTel(Long.valueOf(tel));
if (i == 0) {
User user = new User();
user.setTel(userRegisterVo.getTel());
user.setName(userRegisterVo.getName());
user.setGender(userRegisterVo.getGender());
userMapper.insert(user);
} else {
log.info("------------->注册失败");
return R.error("注册失败,该手机号已注册!");
}
return R.ok("注册成功");
}
- 在Service层的实现类的方法中对
会造成重复CUD的代码放入synchronized(){}方法中
,必须在synchronized(){}方法添加唯一参数:
// 参数为:线程的任务实例(线程类的实例是唯一的时候资源才会遵守幂等性问题==单体项目)
synchronized(线程的任务实例){
// 需要保存的资源的代码(为对sql的cud方法)
}
// 参数为:不相关对象(在当前类中new一个Object对象即可)
synchronized(不相关对象){
// 需要保存的资源(为对sql的cud方法)
}
// 参数为:线程类.class
synchronized(线程类.class){
// 需要保存的资源(为对sql的cud方法)
}
// 参数为:静态常量
synchronized(静态常量){
// 需要保存的资源(为对sql的cud方法)
}
- 单体项目中(
单实例情况下
):synchronized关键字和synchronized()可以互换;
分布式锁:
概念:
解决项目多实例情况下的多线程高并发导致的(幂等性、数据泄露)问题,
注意(单实例不存在多线程高并发问题,只存在单线程高并发问题)
;
解决方案:
- 数据库的悲观锁(在sql语句中添加
for update
可以后对数据进行加锁,这个时候其他线程无法进行CURD操作
)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xxb.user.mapper.UserMapper">
<select id="selectUserLock" resultType="com.xxb.user.pojo.User">
select *
from kss_user
where id = #{userId} for update
</select>
</mapper>
- 数据库的乐观锁(表字段添加一个
版本字段
,在sql语句对版本字段
进行判断,不会进行加锁:如果版本字段
变化了,说明其他线程在此时进行了操作,那么当前这个线程就更新失败了)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xxb.user.mapper.UserMapper">
<update id="updateUserLock">
update kss_user
set amount = amount - #{money},
version = version + 1
where id = #{userId}
and amount > 0
and (amount - #{money}) > 0
and version = #{version}
</update>
</mapper>
- Redis不可重入锁
归功于Redis底层是单线程执行命令,而Java多多线程实例的: (Redis拿到Java实例并上锁 set key value EX 20 NX
不存在则创建返回TRUE无并发,存在返回FALSE有并发
,20表示超过20秒内会自动关闭Java实例锁,注意:Redis分布式锁在关闭锁的时候需要判断key来关闭锁,防止一些方法执行超过20秒后关闭锁时关闭了其他线程的锁
,这里有关闭锁的两种情况:超过20秒后Redis自动关闭/finally代码关闭锁)
/**
* 并发问题使用:Redis非重入锁(拿不到锁就离开【没有排队机制】)
*
* @param userRegisterVo
* @return
*/
@Override
public R registerUserForRedisLock(UserRegisterVo userRegisterVo) {
String tel = userRegisterVo.getTel();
String key = "user:amount:Key:" + tel;// 根据用户id生成的key,会放入redis中
String value = System.nanoTime() + "_" + UUID.randomUUID();// 生成value,会放入redis中
// 关键代码:拿到一个Java实例
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
// 关键代码:(被动释放锁)给实例进行上锁,20秒钟内所有实例的请求和操作,只能执行成功一次(等价于 set key value EX 20 NX)
Boolean isLock = opsForValue.setIfAbsent(key, value, 20, TimeUnit.SECONDS);
log.info(".....................isLock ={}", isLock);
try {
// 不存在(TRUE)没有并发问题则拿到锁修改共享资源,存在(FALSE)有并发问题则离开
if (isLock) {
int i = userMapper.existTel(Long.valueOf(tel));
if (i == 0) {
User user = new User();
user.setTel(userRegisterVo.getTel());
user.setName(userRegisterVo.getName());
user.setGender(userRegisterVo.getGender());
userMapper.insert(user);
} else {
log.info("------------->注册失败");
return R.error("注册失败,该手机号已注册!");
}
} else {
// 不重入锁这里会进
log.info("没有拿到锁离开了!");
System.out.println("客官请慢一点!");
}
return R.ok("注册成功");
} catch (Exception ex) {
return R.error("执行失败!请查看日志!");
} finally {
// 关键代码:(主动释放自己key对应的锁)无论该实例执行正确还是失败,都需要在redis加锁成功后释放锁
if (value.equalsIgnoreCase(opsForValue.get(key).toString())) { // 防止删除别人的锁
log.info("=================opsForValue.get(key) = {}", opsForValue.get(key));
stringRedisTemplate.delete(key);
}
}
}
- Redis + Redisson的不可重入锁/可重入锁(在Redis拿到Java实例的基础上
解决了可重入的问题
以及自己不可重入的方案,且可以指定上锁的key所以在锁回收的时候就不用对key进行判断了
)
/**
* 并发问题使用:Redis + Redisson(可重入锁)
*
* @param userRegisterVo
* @return
*/
@Override
public R registerUserForRedissonReentrant(UserRegisterVo userRegisterVo) {
String tel = userRegisterVo.getTel();
String key = "user:amount:Key:" + tel;// 根据用户id生成的key,会放入redis中
// 关键代码:Redisson客户端申请锁(用户级别的锁),同时也拿到了Java实例
RLock redissonLock = redissonClient.getLock(key);
try {
// 关键代码:设置锁逻辑(获取到锁只有20秒时间执行,到了自动释放锁,防止死锁【001、002、003直到轮到你】)
redissonLock.lock(20L, TimeUnit.SECONDS);
int i = userMapper.existTel(Long.valueOf(tel));
if (i == 0) {
User user = new User();
user.setTel(userRegisterVo.getTel());
user.setName(userRegisterVo.getName());
user.setGender(userRegisterVo.getGender());
userMapper.insert(user);
} else {
log.info("------------->注册失败");
return R.error("注册失败,该手机号已注册!");
}
return R.ok("注册成功");
} catch (Exception ex) {
return R.error("执行失败......");
} finally {
// 关键代码:无论如何都需要释放锁
redissonLock.unlock();
}
}
- ZooKeeper的可重入锁
原理:
- 并发的线程进来(线程A、B、C、N)会在Zookeeper的
临时顺序节点(znode)
下创建对应的顺序的编号
(0001,0002,0003,000N),- 从编号最小依次分给对应的(线程A、B、C、N),线程拿到编号后执行程序访问数据库共享资源,
执行后删除编号
,- Zookeeper通过
watch看门狗机制监控删除编号行为
:行为执行后,将临时顺序节点(znode)下创建对应的顺序的编号分给下一个线程B、C、N;
/**
* 并发问题使用:Zookeeper(可重入锁)
*
* @param userRegisterVo
* @return
*/
@Override
public R registerUserForZooKeeperLock(UserRegisterVo userRegisterVo) {
String tel = userRegisterVo.getTel();
/**
* ZooKeeper原理逻辑:
* A实例---并发的线程1 2 3 X---/useramount/zklock/a_lock 001 002 003 00X
* B实例---并发的线程1 2 3 X---/useramount/zklock/b_lock 001 002 003 00X
*/
// 关键代码:设置锁的力度(用户级别的锁,用户可以各自执行),临时顺序节点的路径和名称
String lockPath = "/tel/zklock/" + tel + "_lock";
// 关键代码:生成临时顺序节点znode,并生成对应的顺序编码
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
try {
// 关键代码:编码给到线程,获取锁判断(20秒判断是否获取到了锁,没获取到锁的话继续等待【001、002、003直到轮到你】)
boolean isLock = lock.acquire(20L, TimeUnit.SECONDS);
if (isLock) {
int i = userMapper.existTel(Long.valueOf(tel));
if (i == 0) {
User user = new User();
user.setTel(userRegisterVo.getTel());
user.setName(userRegisterVo.getName());
user.setGender(userRegisterVo.getGender());
userMapper.insert(user);
return R.ok("注册成功");
} else {
log.info("------------->注册失败");
return R.error("注册失败,该手机号已注册!");
}
} else {
// 注意:这里不进,表示这个ZooKeeper锁是(可重入的)
log.info("没有拿到锁,离开了...");
}
} catch (Exception ex) {
return R.error("执行失败......");
} finally {
try {
// 关键代码:无论如何都需要释放锁
lock.release();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return R.ok("执行完成......");
}