一、分布式锁基本原理和不同实现方式对比
多个JVM共享同一个锁监视器;多进程可见,并且互斥; 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁;
不同的分布式锁实现方案:
二、Redis锁的实现思路
获取锁:通过setNx命令 释放锁:DEL key 命令-----手动释放,或者超时释放,给锁添加一个超时时间;(超时时间的设置需要考量,不能太长,避免类似死锁现象的发生)
/**
* 通过redis解决分布式环境或者集群模式下的一人多单问题
* @param userId
* @param voucherId
* @return {@link Result}
*/
@Transactional
public Result createVoucherOrder(Long userId, Long voucherId) {
// 5、扣减库存
// 5.1 实现一人一单功能 查询当前用户id和优惠券id是否存在
// todo 在高并发的情况下,100个线程同时查,查询到的都是0,所以依然存在同一个用户抢了多个优惠券的情况;
// 由于是插入操作,无法比较,所以最好加悲观锁
// 要保证锁的是一个对象,userId.toString()每次会是一个全新的对象,
// .intern()返回字符串的规范表示,去常量池寻找是否有这个值,确保用户id一样,值就一样
// todo 锁名
String name = "order:";
// 拿到锁对象
SimpleRedisLock simpleRedisLock = new SimpleRedisLock(name + userId,stringRedisTemplate);
boolean tryLock = simpleRedisLock.tryLock(1200);
if (!tryLock){
return Result.fail("不能重复领取优惠券");
}
try {
int count = this
.query()
.eq("user_id", userId)
.eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("你已下过单");
}
boolean sucess = iSeckillVoucherService.update()
// CAS 法 Compare and Switch**:比较修改。在版本号的基础上,
// 既然用version字段前后可以比较得出这条数据是否发生变化,那同样,
// 直接用stock库存本身来比较,stock前后是否发生了变化;
.setSql("stock = stock -1") // set stock = stock -1
.eq("voucher_id", voucherId).gt("stock", 0)
// 乐观锁的缺点:** 成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,
// 其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
// 所以这里这样判断stock > 0即可
.update();
if (!sucess) {
return Result.fail("库存不足");
}
// 6、将数据存入优惠券订单表
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(redisIdWorker.generateOnlyId("order"));
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(voucherOrder.getId());
} finally {
// 最后一定要释放锁
simpleRedisLock.releaseLock(name + userId);
}
}
三、redis分布式锁误删问题
极端情况: 1、业务阻塞导致锁提前释放了; 2、其他线程一上来,业务没执行完,线程1这时候执行完了,把线程2的锁给删掉; 3、所以要在释放锁的时候要判断,锁的标志是否一致;别的线程不能删; 4、解决方案:
四、分布式锁的原子性问题
1、极端情况:就算加了线程判断标志,当要释放锁的时候,其他类似垃圾回收,jvm本身导致线程阻塞;轮到我释放了,但没有释放,触发了锁的超时释放,也会导致其他线程乘虚而入; 2、判断锁标志和释放锁标志是两个动作;需要让他们一起执行
解决方案:通过lua脚本执行多个redis命令;
– 锁的key local key = KEYS[1] – 当前线程标识 local threadId = ARGV[1] – 获取锁的线程标志 local id = redis.call(‘get’,KEYS[1]) – 比较线程标志与锁中的是否一致 if(redis.call(‘get’,KEYS[1]) == ARGV[1]) then – 释放锁 del key return redis.call(‘del’,key) end return 0
/**
* 使用lua脚本释放锁(获取锁标志和删除锁是同步的)
* UNLOCK_LUA
* Collections.singletonList(RedisConstants.LOCK + name) 锁的key集合
* LOCK_THREAD + Thread.currentThread().getId() 线程标识
* @param
*/
@Override
public void releaseLock() {
// lua脚本
stringRedisTemplate.execute(UNLOCK_LUA,
Collections.singletonList(RedisConstants.LOCK + name),
LOCK_THREAD + Thread.currentThread().getId());
}
/**
* 获取线程与删除锁没有同步执行,极端情况下,比如因为JVM本身的垃圾回收,导致线程超时释放锁,其他线程会乘虚而入
*
* @param lock
*/
/*
@Override
public void releaseLock() {
// 获取线程标志
String threadId = LOCK_THREAD + Thread.currentThread().getId();
// 当前线程
String id = stringRedisTemplate.opsForValue().get(RedisConstants.LOCK + name);
// 判断是不是我的锁
if (Objects.equals(threadId, id)) {
stringRedisTemplate.delete(RedisConstants.LOCK + name);
}
}*/
五、Redisson分布式锁的功能介绍
基于setnx实习的分布式有下面的问题: 可重入是指同一个线程可以多次获取同一把锁;比如方法A调B,A要获取锁,b也要获取同一个锁,如果是不可重入的,B就无法获取锁;要等A释放才行,造成死锁的情况; 1、不可重入:同一个线程无法多次获取同一把锁; 2、不可重试:获取锁只尝试一次就返回false,没有重试机制; 3、超时释放 4、主从一致性问题:如果redis提供了主从集群,主从同步(读写分离)存在延迟,当主宕机时,如果从没有同步主中的锁数据,会让其他线程乘虚而入;
解决方法:Redisson分布式锁
六、Redisson可重入锁的原理
类似于ReetrantLock:利用Hash结构,记录线程标识,和获取锁的次数,引入了一个计数器; 方法A里面调方法B,A、B都要同一把锁,A一拿到锁,计数器+1 ,B拿到锁,计数器也+1,B执行完逻辑,计数器-1;A当业务执行完成之后,计数器-1,最后判断计数器的数是否为0,为0 ,说明所有业务执行完成,最后释放锁; 由于代码逻辑复杂,为了保证原子性,所以最后用lua脚本编写,
七、Reddison的锁可重试和看门狗机制WatchDoG
Reddison分布式锁总结: 1、可重入:基于Hash结构,hash里field存储线程标识threaId,value存储重入次数,每一次获取锁的时候,先判断锁是否存在,不存在直接获取锁,如果存在,不代表获取锁失败了,再去判断线程的标识是不是当前线程threaId,是当前线程,可以再次获取,重入次数+1,释放锁的时候重入次数-1,直到重入次数为0,所有业务结束,再真正释放锁;实现锁的可重入,类似jdk的ReetrantLock;
2、可重试:利用信号量和消息订阅Pubsub机制,如果第一次获取锁失败,不是立即失败,而是等待释放锁的消息,获取锁成功的线程释放锁的时候会发送消息,从而被捕获到;当线程得到消息时,就可以重新获取锁,如此反复;超过了等待时间,就不会重试了;由于使用了等待、唤醒这样的方案,cpu的性能也不会过多的消耗;
3、锁超时释放:基于看门狗机制,获取锁成功之后开启一个定时 任务,每隔一段时间重置超时时间;
八、Reddison如何解决主从一致性问题?
利用MultiLock ----联锁; 1、redis主从一致性发生的原因:Redis主节点处理写操作,从节点处理读操作,主从节点需要进行数据的同步,但是因为主从不在一个机器,同步会有延时,如果主节点突然故障了,同步没有完成,redis就会从从节点选出一个新的主节点,但由于主节点的锁没有及时同步,所以新的主节点没有锁,此时其他线程来获取锁也能成功,引发线程安全问题;
利用MultiLock ----联锁 2、必须依次向redis多个节点都获取锁,全部获取了才算成功
|