[实战]线上领取优惠券超时

上个月项目比较紧,导致没有太多时间写博客,今天有时间更新一下博客,将前两天解决的线上问题梳理一下,很简单的一个问题,主要排查过程比较有意思,线上打印的日志也并不多。

一、背景

核心实体:

       优惠券:   每张优惠券必须属于某个活动.

       优惠券活动:   用户可以领取该活动下的优惠券,优惠券是在活动创建时生成的,并非领取时才生成.优惠券活动可以属于某个活动

      优惠券活动组: 多个优惠券活动可以属于同一个优惠券活动组,用户可以领取活动组下所有活动的优惠券。

业务场景:

     用户一次性领取某个活动组下的所有活动的优惠券,每个活动各领取一张优惠券。

问题描述:

    领取方法超时时间设置为3秒,但偶尔会出现超时情况,访问量很少。

二、领取活动组下优惠券方法描述

1. 根据活动组查询所有的活动列表

2. 循环遍历活动执行下面逻辑

   a.查询该活动下一张可用优惠券

   b.将优惠券更新为已领取状态

   c.将用户与优惠券进行绑定

伪代码如下:

//活动组id
String groupId = "11";
List<Activity> list = this.queryAllActivityByGroupId(groupId);
for (Activtiy activity : list) {

    //活动名
    String actSn = activity.getActSn(); 
    //获取可用优惠券
    CouponEntity coupon = getAvailableCouponByActSn(actSn);
    //修改状态为已领取
    int num = updateCoupon(coupon);
    //将用户与优惠券绑定
    this.saveUserCoupon(userId, saveUserCoupon);
}

比较重要的信息是获取可用优惠券,SQL如下:

SELECT  * FROM act_coupon WHERE act_sn='活动编号' AND coupon_status = '可用' LIMIT 1 FOR UPDATE

act_coupon说明: act_sn 添加了索引。该表大概有900万数据,单表。

注意:这篇博额不讨论加索引是否合理,以及方案为什么是使用SQL,而不是使用其他方案。

三、排查过程

第一步:首先该接到该问题的是我同事并非我,同事阅读代码,得出结论是因为获取可用优惠券 SQL执行时间过长导致,但并没有好的优化该SQL方式 ,然后转到我这边让我处理。

第二步: 当我接到该问题时,也是认为该SQL执行时间过程导致,但不太确实该SQL执行时间有多长,于是很自然的添加打印日志。如果我当时不追查一下SQL执行时长,当前情况下解决这个问题可能就走远路了(当时已经开始考虑拆表,同时使用Redis缓存的方案)

第三步:加上日志后,查看耗时,该SQL确实执行时间较长,大概在200ms左右,updateCoupon()方法基本上是在1ms内,saveUserCoupon()大概在3ms左右。于时推算方法执行时长,活动组下面有七个活动,也就是执行7次获取可用优惠券、修改优惠券状态以及保存用户优惠券关表,共计1428ms,并未超3秒。

     注意:大家可能会奇怪为什么不直接在本地使用客户端远程连接mysql执行一下获取可用优惠券的sql,原因:我在本地mysql客户端连到线上从库时,执行一个配置表(数量在100以内)查询时长会在0.7秒左右,也就是这种方法无法准确判断执行时长。

第四步:当线上再次报超时时,查看各方法执时长,发现超时的请求在执行第一次获取可用SQL时,执行时间会比较长,基本上在2秒以上,那为什么会这样呢?其实原因很简单,for update大家一看就知道使用的是悲观锁,并发时会出现等待情况,查询机器日志,确认是发生了并发导致第二个请求会比较慢,因为两次请求的uid不同,所以无法确认是用户快速点击多次导致。

知道原因后,解决方案临时采用乐观锁,即每个活动,每次取出3张可用优惠券,遍历优惠券然后将优惠券状态更新为领取状态,更新成功,则跳出遍历优惠券.更新失败则再取下一张。若3张都未更新成功,则返回领取失败,回滚事务。

该方案上线后,观察3周左右并未再次出现问题。

四、总结

1、不要肓目确认问题发生的原因。除了自认为是因为获取可用优惠券SQL执行较慢导致,还在排查过程中认为是updateCoupon和saveUserCoupon方法执行比较慢导致。实事是打脸的。

2、解决方案并非最优方案,但在我们当前场景下解决了我们的问题。我们目前访问量很小且手中项目时间比较紧。没有大段时间来解决该问题。

3、虽然问题已经解决,但仍然需要思考后续问题。优惠券表增长速度很快,且获取可用优惠券SQL执行时间确实比较慢,需要200ms,个人认为后面可以将缓存先加上,即可用优惠券存在redis中,从redis中取再进行修改,可以抗一段时间。

关于MySQL乐观锁、悲观锁大家可自行查资料,如果有不懂可以留言。我们的访问并非很大,方案有些粗糙,但大家可以讨论一下量大的情况下如何处理,例如京东优惠券的方案。

        

猜你喜欢

转载自blog.csdn.net/c364902709/article/details/82563425