义父们点点赞,球球了,更新不易,qaq
目录
3.6 基于setnx实现分布式锁的问题,引出Redission
4.4Redission超时释放机制(watchdog 看门狗机制)
4.5Redission主从一直性(连锁 multi lock)
一Redis解决秒杀问题
1.1解决秒杀思路:直接上图

注意点:需要为优惠券Id设置唯一性id,且没有明显规律,原因:如果直接使用数据库自增id,可能会造成数据泄露,通过观察可以得到一些店铺的营业数据(比如日常喝奶茶的订单号)
所以需要为优惠券订单设置唯一id:
常见方法:1UUID 2雪花算法 3自定义时间戳+序列号
使用时间戳的好处:方便统计每天,每月的订单量,并且有规律,其次业务区分度明显,不同的业务名使用不同的前缀
@Component
@RequiredArgsConstructor
public class RedisIdWorker {
@Resource
private StringRedisTemplate stringRedisTemplate;
//设置开始时间
private static final long BEGIN_TIME = 1735689600L;
//序列号的位数
private static final int COUNT_BITS = 32;
public long nextId(String keyPrefix){
//生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIME;
//生成序列号
//获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//拼接
return timestamp << COUNT_BITS |count;
}
}
按照图一的逻辑,当并发量较高时,我们很容易看出在扣减库存时会发送超卖风险,导致优惠券超卖。
解决方式有两种:1sychronized锁,但是此锁影响性能
2 redis乐观锁
这里先介绍两种锁类型:悲观锁:认为线程安全问题一定会发生,所以操作前先获取锁
乐观锁:认为线程安全问题不一定会发生,只有在更新数据时去判断有没有其他线程做了修改
1.2乐观锁来解决超卖问题
这里我们使用乐观锁来解决超卖问题:
CAS法(compare and set ,前面讲过ConcurrentHashMap,底层实现的就是CAS锁)
我们在减少秒杀券库存时去判断库存是否>0,如果 <0 ,则停止操作,这里展示关键代码
//是,扣减库存,扣减库存前比较库存是否发生改变
//CAS法compare and set
boolean sucess = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
即在进行更新操作时,进行条件判断: 当前库存是否 > 0,即是否还有余量
此时超卖问题即可解决,非常的神奇啊家人们,CAS非常好用的思想,非常nice!!!
但是此时虽然解决了超卖问题,我们测试时不难发现一人可一直抢购订单,我们并未对每个人抢购优惠券数量做出限制,由此引发下一个问题:一人一单
二、一人一单问题
那解决一人一单问题和前面的思想一致,我们在修改库存前 判断该用户是否下过订单(根据用户的优惠券id和订单id),并且为该用户id加上(Synchronized)锁,保证每个用户只能获取一单并且不影响其他用户抢购。

核心代码:
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
此时我们发现在单机情况下能够解决一人一单问题,但是在集群模式下依旧有问题?为什么呢,因为JVM是通过锁监视器来进行监控的,不同的jvm并不能监控同一个线程,这个时候就需要分布式锁了

三、分布式锁
3.1setnx分布式锁
我们来看看redis分布式锁的解决思路:利用setnx(只有当该key不存在时才能常见,互斥性)命令,为每个线程生成唯一的一把锁,并且为了防止锁超时未删除,我们设置一个ttl自动过期时间
这里给出获取锁的代码,个人觉得这块代码比较好值得学习
//使用BooleanUtil判断, //避免Boolean自动拆箱成boolean时发生空指针异常
3.2锁误删问题

不难看出当线程一获取锁业务阻塞后,此时锁超时自动释放锁(锁被误删),而线程一继续执行业务时,会继续释放锁从而删除其他线程的锁,因为我们的key值为 业务名+ “order:userId”,所以会出现并行情况,造成线程安全问题。
3.3改进分布式锁,解决锁误删
改进的方式非常简单,在我们释放锁之前进行判断,这是否是我们当前线程锁获取的那把锁,如果不是,则说明锁被超时释放了,无需再次删除
3.4redis命令原子性问题
此时便不会造成锁被误删问题,因为我们会进行判定,但是此时我们违反了分布式锁的原子性问题,即我们在判断锁是否一致之后,线程发送阻塞(比如服务直接挂了概率非常小),发生新的线程安全问题,如下图所示

3.5lua脚本解决命令原子性问题
所以我们要让判断和释放锁同时成功或失败,使用lua脚本编写,解决redis命令的原子性问题

即如果 锁中的线程标识等于当前线程标识才进行释放锁

到此,我们基于redis的set nx命令解决一人一单问题到此结束,实现的过程非常麻烦,而且仍然存在许多问题,直接上工具
恭喜前面全部白雪!!!白雪!!!白雪!!!!(开个玩笑,其实我们已经深入了解锁的原理)
3.6 基于setnx实现分布式锁的问题,引出Redission

四、Redission
4.1Redission快速入门
<!-- redission-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.6.128:6379");
return Redisson.create(config);
}
}
4.2Redission的可重入锁原理
利用hash结构替代string结构(记录获取锁的线程和重入次数),hash key-field-value,多存入一个value字段,用来记录获取锁的次数,所以当某一线程进行业务操作时(不止需要一次锁),可以多次获取锁,只有当value= 0时,锁才会被删除
4.3Redission锁重试原理
Redission利用订阅+信号量的功能实现等待,唤醒 获取锁失败的重试机制
即当某线程获取锁失败时,会先判断有没有剩余时间可用,如果有则会进行等待,直到接收到锁释放的信号再进行重试
4.4Redission超时释放机制(watchdog 看门狗机制)
当我们不主动设置锁的主动失效时间时,会触发Redission的看门狗机制(leaseTime=-1),Redission会利用watchdog,每隔一段时间(releaseTime/3),重置超时时间。renewExpiration()(一直循环,永不过期)直到释放锁,unlockAsync()会执行其中的cancelExpirationRenewal(threadId)
4.5Redission主从一直性(连锁 multi lock)
什么是主从一致,即redis集群搭建后会选择一台服务器为RedisMaster,只处理写,其他服务器redis slave只处理读,所以主机需要不停地把信息同步给其他服务器。当主机挂掉后,服务会从从机选出一台作为新的主机,此时之前主机中的锁会丢失,造成线程安全问题。
4.6小结
直接给图,困了要睡觉了家人们,八股文今天看了但是来不及更新了,写博客写太久了,只能先把今天学的给更了。义父们点点赞,球球了