各场景下线程安全的锁
一、同步锁:当在一个java虚拟机多个线程操作一个变量的时候就会出现线程安全问题,这个时候就会用到同步锁。
二、异步锁:就是多个java 虚拟机或者说是服务器,操作同一个变量是,会出现线程安全问题,使用需要使用异步锁来处理。
1)数据库 乐观锁 悲观锁 唯一标示 不推荐使用,容易出现锁表,出现死锁。
2)Redis 分布式锁: 就是设置一个flag标识,当一个服务拿到锁以后立即把对应的标识设置为false 用完后释放锁,并把标识修改为true。
3)使用dubbo zookeeper (共享锁,排它锁),这里就根据自己的情况,共享锁还是会出现阻塞的情况,排它锁就是会生成很多临时的节点,谁先获取最小的序号标识谁就先获取到锁。
分布式锁场景
【redis 是单线程的】
互联网秒杀
抢优惠券
如果此时 用 synchronize 这种JVM级别的锁,只会锁住当前 JVM里的那部分
如果有多个 tomcat 调用同一个服务,比如 stock是 100,那么各个tomcat里获取到的 也是100 ,会造成数据重复 超卖的现象。
public void add(SubjectSettingAddReq req) {
synchronized(this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock -1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else{
System.out.println("扣减失败,库存不足");
}
}
}
Redis 命令
SETNX key value
只在键 key 不存在的情况下,将键 key的 值设置为 value.
若键 key已存在,则 SETNX命令不做任何动作。
SETNX 是 【SET if Not eXists】(如果不存在,则SET)的简写。
返回值:命令设置成功返回1,设置失败返回0。
demo
1. SETNX 原理 加 redis锁
public void add(SubjectSettingAddReq req) {
// 获取锁,如果程序突然挂了,为了避免程序因为中断而造成一直加锁的情况产生,1秒钟后,key值失效,自动释放锁,
// lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK);
// stringRedisTemplate.expire(KEY,10000, TimeUnit.MILLISECONDS);
//以上 两段 写在一起 保证程序的原子性,reids 改成原子操作
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK).expire(KEY,10000, TimeUnit.MILLISECONDS);
if(lock){
// 获取到锁,则查询 最大的sort
int sort = subjectSettingWriteMapper.selectMaxSort(req);
//获取锁之后 业务逻辑处理
}
}
2. 针对程序异常 死锁的问题,加上redis超时时间
public void add(SubjectSettingAddReq req) {
try{
// 获取锁,如果程序突然挂了,为了避免程序因为中断而造成一直加锁的情况产生,1秒钟后,key值失效,自动释放锁,
// lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK);
// stringRedisTemplate.expire(KEY,10000, TimeUnit.MILLISECONDS);
//以上 两段 写在一起 保证程序的原子性,reids 改成原子操作
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK).expire(KEY,10000, TimeUnit.MILLISECONDS);
if(lock){
// 获取到锁,则查询 最大的sort
int sort = subjectSettingWriteMapper.selectMaxSort(req);
//获取锁之后 业务逻辑处理
}
}finally{
// 无论如何,最终都要释放
stringRedisTemplate.delete(KEY);
}
}
3. 以上可以应付一般的并发量,但是在高并发情况,可能导致 锁 永久失效
上面代码设置了1万毫秒(10s)自动对该key解锁,
如果第1个线程由于慢查询的原因需要15s 走完 该代码,其中 try中代码10s,finally中 5s;
第2个线程走完该代码 需要8s,其中 try中代码5s,finally中代码3s;
在高并发情况下,1线程进入 finall时 已经自动对该key解锁,2线程 才开始 对该key上锁 ;
1线程进入 finally 中对该key解锁时 实际是对 2线程 中上锁的key解锁,以此类推。。。 会导致锁 永久失效。
解决方法:用reisson 框架,原理 -- 主线程 开启一个分线程(后台线程)将锁延迟 三分之一 时间 再释放
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
public void add(SubjectSettingAddReq req) {
RLock redisLock = redisson.getLock(KEY);
try{
// 获取锁,如果程序突然挂了,为了避免程序因为中断而造成一直加锁的情况产生,1秒钟后,key值失效,自动释放锁,
// lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK);
// stringRedisTemplate.expire(KEY,10000, TimeUnit.MILLISECONDS);
//以上 两段 写在一起 保证程序的原子性,reids 改成原子操作
//Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK).expire(KEY,10000, TimeUnit.MILLISECONDS);
redisLock.lock(10, TimeUnit.SECONDS));
if(!lock){
//return error;
}
// 获取到锁,则查询 最大的sort
int sort = subjectSettingWriteMapper.selectMaxSort(req);
//获取锁之后 业务逻辑处理
}finally{
redisLock.unlock();
// 无论如何,最终都要释放
//stringRedisTemplate.delete(KEY);
}
}
(要注意redis 主从架构 redis 锁失效的问题:Redis(Master) 中锁 失效,但还未同步到 Redis(Slave) 中,导致其他线程过来时拿不到锁)