目录
一、前言
天机学堂day11的主题是领取优惠券的优化,分为分布式锁和异步领券两大部分。本博客主要用于记录个人学习过程,方便后续复盘,最后会包含我个人的练习答案,仅供参考
二、分布式锁
1、简单分布式锁
1)代码
这部分如果不想做可以不做,反正简单分布式锁也是存在问题的,想做的话复制UserCouponServiceImpl黏贴并改名为UserCouponRedisServiceImpl

把原来的UserCouponServiceImpl的@Service注解注释掉

在新的UserCouponRedisServiceImpl中把检验并生成一个用户券的代码修改为简单分布式锁

2)测试
老样子把要测试优惠券的issue_num改为0,把user_coupon的表数据清空。启动服务,打开jmeter,把单人下单抢券的线程数从1改为500在启动

我测试发现和视频中的并不一样,我这里还是出现了并发问题的,不过简单分布式锁本来就是可能出现并发问题的,我反复看了代码发现就是一样的,可见简单分布式锁并不能完全保证并发安全


2、Redisson实现分布式锁
1)代码
把Redisson快速入门的复制过来的@Configuration注解注释掉,因为这个只是快速入门的配置,实际项目中在tj-common中已经配置好了更完善的RedissonConfig

项目已经配置好的RedissonConfig有个@ConditionOnMissingBean注解,如果不把刚刚的入门配置注释掉就不会用这个完善的配置了

复制UserCouponRedisServiceImpl并改名为UserCouponRedissonConfig

把UserCouponRedisServiceImpl上面的@Service注解给注释掉

修改UserCouponRedissonConfig中的代码

2)测试
老样子把要测试的优惠券issue_num改为0.然后把user_coupon表数据全部清空。启动服务,打开jmeter通过单人下单抢券进行测试。测试之后查看数据库可以看到相比于简单分布式锁,Redisson实现的分布式锁就不会出现并发安全问题


3、通用分布式锁组件
3.1.实现
1)代码
复制自定义锁注解MyLock

复制自定义锁切面类MyLockAspect

复制UserCouponRedissonServiceImpl再改名为UserCouponRedissonCustomeServiceImpl

注释掉UserCouponRedissonServiceImpl上面的@Service注解

修改UserCouponRedissonCustomeServiceImpl中的代码


2)测试
老样子先把数据库中测试优惠券的issue_num改为0再把user_coupon表中的数据全部清除。启动服务,打开jmeter测试单人下单抢券。测试之后查看数据库没有出现并发问题,测试成功


3.2.工厂模式切换锁类型
1)代码
复制自定义锁类型枚举类

复制自定义锁对象工厂类

在自定义锁注解中加入lockType

修改自定义锁切面类中原来写死的获取锁对象代码,改为根据工厂模式,获取用户指定类型的锁对象代码

在serviceImpl的@MyLock注解上加上选择锁类型的代码

3.3.策略模式切换锁失败策略
1)代码
复制自定义失败策略的枚举类MyLockStrategy

在自定义锁注解中加入lockStrategy

修改在切面类中原来写死的策略,修改为采用策略工厂模式,获取用户指定的失败策略

在serviceImpl的代码上指定失败策略

3.4.基于SPEL的动态锁名
1)代码
在serviceImpl的注解的name最后加上:{userId}

在切面类中先把在线文档中的相关代码直接复制进来

还是在切面类中,加上解析锁名称的代码,记得下面传的名称参数也要改

2)测试
不再赘述,直接放成功测试结果


三、异步领券
1、优化思路分析

2、优惠券缓存
2.1.缓存数据结构

2.2缓存key前缀

2.3.立即发放优惠券时添加缓存
1)代码
在CouponServiceImpl的发放优惠券方法(issueCoupon)中,在如下图的位置加上添加缓存的代码

视频中完成到这样就结束了,但是查看在线文档可以发现它又封装了一个cacheCouponInfo方法,所以我们也跟着在线文档再一起来做

这里不要想当然的就只写cacheCouponInfo这一行代码,如果只写这一行就会出现问题了(后面解释可能有点绕,不想看就抄答案就行)。因为立即发放的优惠券coupon(原代码这个对象是根据id查出来的)是没有传领取开始时间的,只传了领取结束时间(上面tmp补充了领取结束时间,但是这是coupon不是一个对象)。所以setIssueBeginTime这一步是必要的,但是可以不写在这里,因为它实际传来传去就是一个now,实际也可以在cacheCouponInfo里面加个LocalDateTime.now直接传。至于setIssueEndTime实际是多此一举,因为coupon只有可能不传领取开始时间,一定会传领取结束时间,但是在线文档答案这么写我就顺着它了,这些小点也没必要纠太深

2.4.定时开始发放优惠券时添加缓存(练习)
1)注意
这里的延时发放优惠券就是之前的课后练习,我这里注释写的是定时开始发放优惠券,还是老样子如果你没有完成之前的作业,要么看我之前博客的课后练习记录要么跳过不写。这里实际上就是在最后加上批量添加缓存的代码


2)代码
在原来定时开始发放优惠券的代码上加上批量缓存的代码,这里因为之前的cacheCouponInfo方法在couponServiceImpl中并且还是private的。当然可以硬注入过来再改public再循环里面套cacheCouponInfo,但是没必要,还不如在这个类里面自己创建新方法还见名知意一点


3)测试
启动服务,打开管理端先新增两个测试的优惠券(新增没有什么要注意的)。主要在发放的时候记得选择定时发放,然后前端是会校验你输入的领取开始时间必须在当前时间之后,所以你把领取开始时间设置为当前时间过两三分钟,等到过了时间再到任务调度中心执行一次定时开始发放优惠券



等了几分钟,已经过了定时发放的领取开始时间了,所以打开任务调度中心执行一次定时开始发放优惠券

首先打开coupon表大概看一下刚刚新增优惠券的id

执行之后看控制台输出没有什么问题,批量发放和批量缓存的日志都有

打开Redis可以看到批量缓存的两个数据都有,并且缓存的数值都没有什么问题


2.5.暂停优惠券时删除缓存
1)注意
暂停优惠券是之前的某次课后练习的接口,参考在线文档和master分支的都可以,我只不过是方法名和它们不太一样,下面是原接口的参考代码(没完成建议看我之前博客的课后练习答案记录)

2)代码

2.6.定时结束发放优惠券时删除缓存(练习)
1)注意
同样这部分也是之前的课后练习,在原基础上加一点代码,如果没做的话要么看我之前博客答案,要么跳过


2)代码
批量删除的代码比较简单,就不单独创建方法封装了

3)测试
先把刚刚批量缓存的两个测试优惠券的领取结束时间改到当前时间之前

修改之后启动服务,打开任务调度中心,执行一次定时结束发放优惠券

执行成功之后可以看到批量结束和批量删除缓存的日志输出

打开redis可以看到刚刚批量缓存的两条数据被删除了,下面这个是整个异步领券的测试数据(原本有3个prs:coupon和1个prs:user:coupon现在删了两个prs:coupon),可见测试成功

3、异步领券
3.1.定义MQ消息规范
1)代码
在MqConstants中添加如下两处:


3.2.基于Redis的领取资格校验
1)代码
首先认识一下等会需要用到的Redis的键前缀,这个常量类之前就定义好了,不需要新增只是知道用哪个加点注释(记得这两个键前缀都是拼接优惠券id,没有拼接用户id的)

然后老样子,将UserCouponRedissonCustomeServiceImpl复制一份改名为UserCouponRCMQServiceImpl(这个改名我保留RedissonCustome简写为RC再加上MQ,和老师的命名不同,大家按喜好修改)。然后这里我提一嘴,我之前看视频老师都是把之前的代码全选注释然后我感觉不优雅就只注释掉了@Service,之前一直没遇到过问题,结果后面监听到mq消息后异步生成一个用户券方法添加到接口后,我发现之前所有只注释掉@Service的serviceImpl都会爆红(简单来说就是它们都各自需要实现这个接口,所以大家还是和视频保持一致,把代码全选注释)


下面开始正式改进发放优惠券的代码,第一处改进是把原来根据id查询优惠券(查db)改为从Redis缓存中查询优惠券信息。因为这里查的是coupon信息,所以后续的校验(我这里校验指的是1.2~1.6步,不包括校验限领数量)都是从coupon中取不需要改动(就校验库存要改一下),实现了由db查询改进为Redis查询

这个queryCouponByCache方法单独封装一下

第二处改进是把校验库存改变一下,原因我在1.6的注释写的还是比较清楚的,就是Redis少存了issue_num字段,然后只用存total_num字段但是这个字段的意义就改变了

第三处改进是把检验限领数量前置,即与更新优惠券已领取数量 + 1、新增一个用户券的逻辑进行分离。之前是把校验限领数量和更新优惠券已领取数量 + 1和新增一个用户券直接封装在一个方法里面,现在是把更新优惠券已领取数量 + 1和新增一个用户券改为mq异步实现。可能有点难有点绕,大家慢慢理解一下,所以下面的1.7和1.8就是之前的检验限领数量前置出来,至于1.9实际上还是刚刚total_num实现校验库存的附加逻辑

3.3.监听MQ并领券
1)发送MQ消息的代码
先把资料中的UserCouponDTO复制进来,它封装了mq的消息内容

在领取优惠券方法的最后加上发送消息到mq的代码,消息规范在前面已经完成了

2)监听MQ消息并领券的代码
新建一个handler包,再创建如下的listenCouponReceiveMessage

监听到消息之后就是更新优惠券的已领取数量 + 1和新增一个用户券。实际上就是之前的checkAndCreateUserCoupon去掉校验限领数量的逻辑、更新兑换码状态和兑换人(因为这里不需要),这里视频老师改的方法名为checkAndCreateCouponNew,我个人认为不见名知意,所以自己改成了createUserCouponAfterMQMessage,然后如果梳理不清楚逻辑的话还是建议看懂我的注释

这个方法一开始肯定是直接把checkAndCreateUserCoupon直接复制过来再修改的,看我的关于注解的注释就知道这个自定义锁注解不再应该加在createUserCouponAfterMQMessage这个方法上了,所以把自定义锁注解加到领券优惠券的方法上(传个name,其他的不传保持默认就行)


然后这里提一嘴我这里一开始复制过来的时候发现第2步没有int返回值,也没有if判断,就只有couponMapper.incrIssueNum这行代码。后续我翻了一下视频和在线文档,发现视频里老师讲的时候没有修改这个地方,而在线文档是修改了这个地方,所以大家如果也是跟着视频做发现这里不太一样的改一下mapper就行

最后修改一下bootstrap配置如下:

4、整个异步领券的测试
1)提前修改视频测试的bug
首先因为我是先看完视频测试再自己测试,所以这里就先把一开始视频测试出现的bug直接改了。就是把领取优惠券方法中的校验优惠券状态的代码给注释掉,至于原因在注释上写的很清楚了

2)清理一下MySQL和Redis的数据并保持统一
把之前测试分布式锁的优惠券信息删除干净,先把coupon中测试分布式锁的优惠券issue_num由1改为0(我这个之前一直用双十奶龙活动促销),再把user_coupon表的数据删除干净


下图中蓝色部分是我目前coupon中原始的优惠券,其余看名字也知道是我自己学习阶段搞的。然后可以看到Redis中也有一部分原始的缓存数据,我比对了一眼,发现只有1985结尾的优惠券是对应的,其余缓存优惠券id在coupon里根本找不到(也可能是我之前删除优惠券删掉了)


再查看1985结尾的优惠券,可以看到它的issue_num为0。它的优惠券缓存中totalNum为0说明它根本没发过,而另一个缓存的value为1(这个1是提前加的,加了之后为1刚好等于userLimit说明应该是刚好领了一张券),同时我们之前测试分布式锁早都删除干净记录好几次了。所以最终我得出了结论是Redis这些原始缓存数据除了影响我的判断没有任何用处,所以都删了



3)正式测试
启动服务,打开管理端的前端,新增一个优惠券。注意推广方式要选手动领取,毕竟兑换码相关的缓存还没做

选择立即发放,发放成功之后就可以观察是否有缓存数据了。可以看到MySQL和Redis中的优惠券缓存对应且没有问题



然后在用户端jack领取一下刚刚新建的优惠券.领取成功之后可以看到优惠券信息缓存的totalNum减了1没有问题,同时用户优惠券已领取数量缓存也出现了并且value为1没有问题,然后user_coupon表出现了一条数据,最后是coupon表中的issue_num也成功加1(改的是Redis的totalNum,MySQL的total_num改进之后是不会再变的)





4)压测
上面测试没有问题,就把数据还原回去进行压测。把totalNum改回10(DataGrip改不了换一个Redis连接工具改),把用户券已领取数量缓存删了,把user_coupon表数据删了,把issue_num改回0。改完之后打开jmeter,记得复制测试的优惠券id改http请求参数再测试

压测结束可以看到issue_num变为1并没有超卖。优惠券信息缓存的totalNum变为9没有问题。用户券已领数量缓存这里由于逻辑是提前加1并且它校验都是判断value是否大于userLimit,所以这里value数值并不奇怪很正常。最后user_coupon只有一条没有问题,测试全部成功




四、课后练习(练习但是文档很详细)
一开始我看了一会,说实话让我自己写我是真写不出来,也只能跟着在线文档一步步做了(能看懂在线文档并梳理清楚都不错了,自己是真写不出来)。但是跟着做的过程还是有一点我自己的见解,所以我这里建议大家可以直接去看在线文档做也可以看我的来做
1、异步的兑换码领券


1.1.缓存兑换码
找到ExchangeCodeServiceImpl的生成兑换码方法,发现其实之前视频就已经加上了代码还加了个注释。所以我这里就是改了下注释让自己更好理解,没动代码(如果你没有的话就自己加上代码)


1.2.清理一下MySQL和Redis数据并保持统一
这部分主要是我按照自己的情况进行的分析,大家和我不一样很正常,但是了解一下删除部分原始的无用数据还是可以的。由于我们之前学习生成兑换码的方法时就加上了写入Redis的缓存,所以redis是有数据的,对于下面的数据我可以肯定的说score为6000和6100的是原始的无用数据可以直接删除(我解释一下,我是直接按照优惠券id去找的,发现6000和6100的优惠券根本找不到,然后6200这个是我自己新增的双十M超人这个只不过我当时测试的时候忘记把serialNum改为0了所以就续上了)。所以最后结论就是删6000和6100这两行(纯原始数据大家可以大胆删,DataGrip删不了redis数据换其他的软件),然后是我个人错误删6200并且直接把双十M超人删了(这个大家不用管)


同样exchange_code里面也是有一堆原始数据,从id为4001开始往后其实都是原始数据可以大胆删除(从create_time都能看的出来),这些数据放这除了影响判断没有任何好处,真测试都是现造数据

1.3.改造兑换码领券功能
附上我自己的改造前的兑换码领券的代码如下:


然后下面是在线文档中改造之后的代码,我自己比对了一下发现改动主要在以下几处:1、从db查兑换码再判断兑换码是否存在 -> 改进为从redis缓存中读取优惠券id,通过判断优惠券id是否存在关联兑换码是否存在 2、判断是否过期之前从查出来的兑换码中取,现在改为从兑换码对应的优惠券取(兑换码过期时间就是优惠券的领取结束时间) 3、校验优惠券限领数量也是和之前异步领券一样不再是和后续逻辑全封装到一起,而是写在这个方法内 4、发送消息到mq,把后续的更新优惠券已领数量+1、生成用户券、更新兑换状态和兑换人都放到监听消息后异步去执行
说实话一开始我梳理这部分逻辑还感觉代码不太对,想着不查兑换码就得不到兑换码的过期时间,同时redis也没有缓存过期时间,然后怎么校验过期呢?后面翻之前的代码才知道兑换码的过期时间就是优惠券的领取结束时间,所以这里优化的逻辑还是有点巧妙的,一个是过期时间另一个是优惠券和兑换码的绑定关系,就在加redis缓存的情况下改进到不需要查db


改造之后的兑换码兑换券方法代码如下:



IExchangeCodeService添加如下代码:

ExchangeCodeServiceImpl代码如下:
1.4.监听到mq消息后异步生成一个用户券的个人修改
首先我发现这里在线文档是没有给出PromotionMqHandler的代码的,然后我就去master分支看了一下。再结合刚刚的发送消息到mq的代码看的出来黑马参考答案的代码就是领取优惠券和兑换码兑换优惠券都用同一个queue和key

因为我写代码喜欢加很多注释(下图是我原来的代码),然后我这里觉得分开会比较优雅一点,所以我这里就按自己理解修改一下代码

我在MqConstants中加上了兑换码兑换优惠券的key

然后把刚刚发送消息到mq的代码的key给修改了

在PromotionMqHandler里面添加一个监听的方法,然后到这里我们可以知道之前的checkAndCreateUserCoupon方法已经完全不用了,因为分离了部分逻辑,这里还是用createUserCouponAfterMQMessage这个后面新创的方法,但是我们这里要加更新兑换码状态和兑换人的逻辑

所以我的createUserCouponAfterMQMessage方法如下,上面是修改了一些注释,然后下面就是在传了serialNum的情况下更新兑换码状态和兑换人的代码


2、基于LUA脚本的异步领券
2.1.领券脚本
在resource包下创建lua包再创建两个lua脚本(创建lua脚本需要下载插件,之前黑马点评学习过的)


一个是领取优惠券使用的lua脚本

另一个是兑换码兑换优惠券使用的lua脚本

2.2.改造业务
由于又是一次大改动,所以为了记录我把UserCouponRCMQServiceImpl复制改名为UserCouponMQLUAServiceImpl,然后把之前的UserCouponRCMQServiceImpl给全部注释掉

把通过静态代码块加载脚本的代码直接复制进来,ClassPathResource导springframework的包

改造领取优惠券的业务,由于之前的第1步的代码太多注释了而且加/**/都老是和其他注释关联,所以干脆直接删干净(反正前面的每个版本都有记录)然后按在线文档来改了

改造兑换码兑换优惠券的业务


3、测试
3.1.测试遇到的问题(看就行别做)
打开exchange_code可以看到第一个兑换码之前测试时用过,因为前面我们清理数据造都把user_coupon清干净好几次了(已兑换user_coupon本来应该有一条数据,但是我们之前清user_coupon没改这个兑换码的相关数据,所以为保证数据一致就来改这个兑换码相关数据),所以这里还是用这个兑换码测试,但是把status改为1,把user_id改为0

把这个对应优惠券的issue_num也从1改为0

还有判断兑换码是否已兑换是用setbit来校验的,所以redis里面的coupon:code:map也要改,把数字1改为0

启动服务,打开用户端jack的前端界面,输入刚刚的兑换码

然后兑换发现一直出现活动未开始的错误信息,我重新回去看了看这个对应优惠券的领取开始时间和兑换码开始发放时间都没有问题

然后我就直接去看复制过来的lua脚本和错误信息的配置,大概意思就是如果redis缓存里面没有prs:coupon:优惠券id就会return 3就对应活动未开始的错误信息。这就能解释了,因为我们刚刚兑换码对应的优惠券是在缓存优惠券之前添加的,所以根本没缓存,测试还是得新增优惠券再测试(不过我总感觉lua脚本这些错误信息的名字取的有点逻辑不通,可能我后续看懂再研究一下)


3.2.正式测试
刚刚了解了出现问题的原因是lua脚本校验要有优惠券缓存,所以现在打开管理端正式测试新增一个优惠券并发放(注意选指定发放不然没兑换码)



通过coupon:code:range可以知道刚刚新增优惠券的第一个兑换码的自增id为551

在兑换码表中找到该兑换码并复制一下

确认现在该优惠券信息是有缓存的

然后打开jack的用户端,老样子用兑换码兑换一下(忘记截图了),兑换成功之后可以看到控制台是有日志输出的,看着都没有什么问题

打开兑换码表,可以看到兑换码状态和兑换人都更新了

打开用户券表也可以看到生成了一个新的用户券(前面那个是之前测试异步领取优惠券的)

再打开位图也能看到修改了,全部测试成功

3.3.关于自定义分布式锁和LUA脚本改进的一些想法





5179

被折叠的 条评论
为什么被折叠?



