目录
介绍
最近在拆微服务的过程中遇到了自己刚入职公司时接的一个需求。
需求:用户领取优惠券
一个用户课堂下,可以创建N个课程,用户可以基于课程创建优惠券(可以针对一节课创建多个优惠券,每个优惠券可以最多领取一次),优惠券创建时会指定一定的数量
拿到这个需求要做技术设计的时候,我们很容易的想到以下几个关键点:
- 优惠券的数量是一定的,不能超额发放
- 用户有且只能领取一张
这个时候作为一名职业的码农,我们能很快的想到一个关键的问题就是,高并发的情况下去领取优惠券,如何能保证上面两个关键点?
我们假设创建了a课程的一种优惠券总数是s,那么sum就是一个并发中的资源共享变量,那么我们的逻辑简单就是
int sum = 0; // 共享变量
if(用户已经领取了优惠券 || sum <= 0) {
return;
} else {
【sum--】
}
【sum--】当发生并发时这个怎么处理?无非不过加【锁】 呗,加锁的目的是让本应该并发的操作,线下执行,这样就不会有共享资源变量竞争问题,在分布式系统性有以下几种加锁方式
【注意】单机不考虑(synchronized),这里主要讨论分布式
redis分布式锁(本篇重点讨论)
zookper分布式锁(本篇不讨论)
基于数据库(mysql)实现分布式锁
通过乐观锁和悲观锁的方式实现
乐观锁:不做详细讲解,不明白的可以去看一下,点击此处
悲观锁:mysql使用innodb存储引擎是支持行锁的
select ... from table where ... for update;
update table set ....
缺点:上面两种方式因为依赖于数据库,明显存在性能问题,生产环境中像领取优惠券类似的高并发场景不推荐使用
优点:依赖mysql,编写简单,维护容易,不需要引入其他技术
基于redis分布式锁
使用redis实现分布式锁,redis本身提供了setnx这个命令,但是让我们自己去实现会有很多坑在里面,下面我们就来看看这些坑。
下图代码是简单的实现了一个领取优惠券的代码:
大家看看上面代码有什么问题?有的小伙伴说,你这个效率太低,第一个请求进来后程序执行到
Boolean isExist = redisTemplate.opsForValue().
setIfAbsent(key, String.valueOf(System.currentTimeMillis()));
后,其他线程都没办进来了,本来优惠券还有很多,直接就返回了,不能领取,这里可以使用一个while循环,去抢锁,或者可以利用分段锁的思想去做,提高并发。
分段锁:把一张优惠券分成多张去领取,比如:优惠券A,一共100张,我们的key就可以设计成,A_1, A_2, A_3, A_4 每个25张
除了上述问题,还有一个很关键的问题就是:
Boolean isExist = redisTemplate.opsForValue().
setIfAbsent(key, String.valueOf(System.currentTimeMillis())); // 第一步
redisTemplate.expire(key, 10, TimeUnit.SECONDS);// 第二步
这两行代码不是原子的,假如我系统直接宕机在执行了第一步后,这个时候我们的锁就没办法释放,解决办法也很简单
Boolean isExist = redisTemplate.opsForValue().
setIfAbsent(key, String.valueOf(System.currentTimeMillis()), 10, TimeUnit.SECONDS);
API提供给我们一个原子的操作,这样不就可以了吗?现在我们的代码换成了下面,这个时候大家看看还有问题吗?
我们看下面一种情况,假如我们的【其他业务操作】执行出于某种原因(慢sql或者服务器卡顿等等)执行时间太长,超过我们预定的可以过期时间10s,那么就会出现下图的情况,当前线程释放了本应属于其他线程的key。
聪明的伙伴一下子就知道如何处理当前情况,在处理删除的时候判断一下是不是当前线程生成的value不就可以了,所以代码又跟新为下面。
但是这样子并没有解决我们的key超时过期问题啊,还是会有其他线程进来,你是不是在考虑我们把10s设置成20s,30s,100s,但是这样并没有解决问题的本质,就算100s真的可以,那么服务宕机后其他线程要等好久key才会过期,其实这里的解决办法是:锁续命
【锁续命】:当线程开始执行时,我们在fork一个子线程去监控当前线程,比如我们的过期时间设置的是10s,我们每过5s,去给当前key,续命成10s,直到线程执行结束。
这样是不是很麻烦啊,写好多代码去维护,其实redission帮助我们解决了上面的问题,所以我们直接使用就行了,别自己去实现,还有的小伙伴脑子里现在考虑,你这太啰嗦了,我写个lua脚本不就行了,lua脚本确实能保证原子的去执行,还是老问题,~麻烦~,我们接下来使用redisson看看这里怎么实现。
基于redission分布式锁
我们先来看redis官方对于分布式锁的描述:官网也推荐我们,当使用java开发时,使用redisson。
官方针对各种语言都提供了对应的分布式锁实现,下面提供了java implementation + spring boot 的整合链接,有兴趣的可以去看一下。
redisson-spring-boot-starter Redisson Spring Boot 脚手架
【注意】:当利用redis实现分布式锁时,推荐使用Redisson,框架帮我们避免了大多数坑,可以通过阅读其底层实现学习。
下面使用redisson来改善一下我们的既有代码,如下图:
redisson:是Redisson的实例;
看完代码,是不是觉得很简单,大家会特别疑惑,就这几行代码就能处理掉我们上面说的那么多问题吗,其实redisson框架本身不简单,其中实现分布式锁一块,底层也是使用lua脚本来实现的。
下面是redission实现分布式锁的原理图:基本上就是我们上面分析的,其实有兴趣的小伙伴去读源码
第一次写博客有什么不到位的地方或者问题,大家多多见谅~,私底下可以联系我。