Java秒杀系统(十五):基于Redisson的分布式锁优化秒杀逻辑

作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。


摘要:本篇博文是“Java秒杀系统实战系列文章”的第十五篇,本文我们将借助综合中间件Redisson优化“秒杀系统中秒杀的核心业务逻辑”,解决Redis的原子操作在优化秒杀逻辑过程中出现的部分瑕疵。

内容:Redisson,字如其名,是搭建在缓存中间件Redis的基础之上的一款综合中间件,除了拥有Redis本身提供的强大功能之外,还提供了诸如分布式锁、分布式服务、延迟队列、远程调用等强大的功能(从名字就可以看出来了:Redis + son,犹如Redis的儿子,儿子不仅继承了老爸强大的血脉,而且还自己修炼、发展出了属于自己的一套本领)


在本篇文章中,我们将使用Redisson中间件其中一个强大的功能组件“分布式锁”,用以解决秒杀系统中高并发产生的多线程对于共享资源/代码块的访问所导致的“并发安全”问题!

而之所以需要Redisson这一组件,是因为在上一篇文章中,我们在采用Redis解决秒杀系统中出现的“库存超卖”、“重复秒杀”等问题时所对应的代码存在着瑕疵,即在使用Redis的SetNX操作之前、而还没来得及执行Expire操作的时候,Redis的节点如果恰好出现宕机或者服务不能用的情况,那将会导致相应的Key永远存在于缓存中,而处于“被锁死”的状态!

Redisson分布式锁的出现可以很好地解决这种问题,其底层的实现机制在于“Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期”,除此之外,Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间,即超过这个时间后锁便自动解开了。

接下来,我们将基于SpringBoot搭建的秒杀系统整合Redisson,加入其相关的依赖以及配置,并使用其“分布式锁”组件彻底解决秒杀过程中出现的“库存超卖”以及“重复秒杀”等问题。

(1)首先,需要加入Redisson的依赖,版本号为3.8.2,如下所示:

<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>

然后需要在配置文件application.properties中加入Redis服务所在的Host、Port等信息,如下所示:  

#spring.redis.password=
redis.config.host=redis://127.0.0.1:6379

(2)紧接着,是基于Spring Boot自定义注入Redisson相关操作的Bean组件,其中,主要是RedissonClient 操作组件的自定义注入,其完整源代码如下所示:  

/**
* redisson通用化配置
* @Author:debug (SteadyJack)
* @Date: 2019/7/2 10:57
**/
@Configuration
public class RedissonConfig {

@Autowired
private Environment env;

@Bean
public RedissonClient redissonClient(){
Config config=new Config();
config.useSingleServer()
.setAddress(env.getProperty("redis.config.host"))
.setPassword(env.getProperty("spring.redis.password"));
RedissonClient client=Redisson.create(config);
return client;
}
}

(3)前期工作已经准备完毕,接下来我们需要将其应用到秒杀系统中 秒杀的核心操作逻辑,在KillService服务类中我们开辟了一个新的处理方法,即killItemV4,其完整的源代码如下所示:  

@Autowired
private RedissonClient redissonClient;

//商品秒杀核心业务逻辑的处理-redisson的分布式锁
@Override
public Boolean killItemV4(Integer killId, Integer userId) throws Exception {
Boolean result=false;

final String lockKey=new StringBuffer().append(killId).append(userId).append("-RedissonLock").toString();
RLock lock=redissonClient.getLock(lockKey);

try {
//TODO:第一个参数30s=表示尝试获取分布式锁,并且最大的等待获取锁的时间为30s
//TODO:第二个参数10s=表示上锁之后,10s内操作完毕将自动释放锁
Boolean cacheRes=lock.tryLock(30,10,TimeUnit.SECONDS);
if (cacheRes){
//TODO:核心业务逻辑的处理
if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
ItemKill itemKill=itemKillMapper.selectByIdV2(killId);
if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){
int res=itemKillMapper.updateKillItemV2(killId);
if (res>0){
commonRecordKillSuccessInfo(itemKill,userId);

result=true;
}
}
}else{
throw new Exception("redisson-您已经抢购过该商品了!");
}
}
}finally {
//TODO:释放锁
lock.unlock();
}
return result;
}

从该源代码中,我们主要是使用了Redisson分布式锁中的“可重入锁”组件,其使用需要经过如下几个步骤:

A.需要尝试去获取锁,其对应的代码以及注释如下所示:

//TODO:第一个参数30s=表示尝试获取分布式锁,并且最大的等待获取锁的时间为30s
//TODO:第二个参数10s=表示上锁之后,10s内操作完毕将自动释放锁
Boolean cacheRes=lock.tryLock(30,10,TimeUnit.SECONDS);

B.在获取到锁之后,即cacheRes=true,即可进入秒杀核心业务逻辑的处理;同时在处理完成之后,需要释放锁,如下所示:  

//TODO:释放锁
lock.unlock();

(4)至此,基于Redisson的分布式锁解决高并发业务场景下,并发多线程对于共享资源/共享代码块的并发访问所出现的并发安全的问题的代码实战已经完毕了!

我们接下来进入压测环节,仍然以之前的测试用例为例,即killId=3的待秒杀商品的可秒杀数量total=6,可以随机选取的用户Id列表的总数为10个,其取值为10040~10049,则理论上最好的结果是:total最终变为=0,同时item_kill_success有6条用户秒杀成功后生成的订单记录。

这个时候,我们尝试将线程组中并发的线程数调整为10w,点击启动按钮,稍等片刻,观察控制台的输出信息以及item_kill和item_kill_success的数据库表,查看其最终的记录结果,如下图所示:


对于这一结果,其实可以说是预料之中了!

Redisson的分布式锁确实可以在高并发业务场景/多线程高并发 场景下起到举足轻重的作用。而在现实生活中,其实Debug也是建议各位小伙伴可以去研究这一综合中间件,它完全可以替代Redis在项目中的使用,而且其提供的数据结构以及使用方式跟JavaSE中的数据结构很类似,比如List、Set、Map、Queue等等都可以在Java中找到相应的踪影(而事实上Redisson的许多分布式组件跟数据结构正是基于Java中相应的数据结构来实现的)! 

补充:

1、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,该秒杀系统对应的视频教程的链接地址为:https://www.fightjava.com/web/index/course/detail/6,可以点击链接进行试看以及学习,实战期间有任何问题都可以留言或者与Debug联系、交流!

2、另外,Debug也开源了该秒杀系统对应的完整的源代码以及数据库,其地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill 记得Fork跟Star啊!!!

3、最后,不要忘记了关注一下Debug的技术微信公众号: