前段时间收到一个优惠券兑换码的需求:管理后台针对一个优惠券发起批量生成兑换码,这些兑换码可以导出分发到各个合作渠道(比如:抖音、京东等),用户通过这些渠道获取到兑换码之后,再登录到我司研发的商城,使用兑换码兑换获得对应的优惠券。
整个需求大致分为两个部分:(1)批量生成兑换码;(2)使用兑换码兑换优惠券。接下来的几篇文章将针对批量生成兑换码功能实现过程中碰到的一系列问题进行分析描述,以便读者再碰到类似问题,可以快速解决。
文章系列如下:
在此之前,先简单介绍商城技术架构:商城后端服务均采用SpringCloud框架开发,数据库主备,商城所有服务共用一个数据库,数据库持久化框架为MybatisPlus,所有服务采用K8s技术进行部署和治理。
在《事务同步回调问题》一文末尾,笔者抛出了一个问题:由于兑换码生成过程比较占用资源,所以全平台保证同时只能有一个生成任务在执行。根据生成业务流程图如何保证插入兑换生成记录的并发安全?
根据上述流程图可知,如果两个用户同时对两个优惠券发起兑换码生成请求,按照通常编码方式,可能会出现两个任务同时执行的情况。通常编码编码方式如下:
if(dhCodeService.listDoingStatus().size() > 0) {
dbCodeBatchService.insert(dbCodeBatch);
}
上述代码存在线程安全问题,有读者可能会建议这段代码加一个synchronized即可,但大家不要忘了,咱们的系统是分布式系统,多服务实例,synchronized只能解决单进程(非分布式)场景中的并发问题。
针对上述问题,有以下解决方案:
(1)分布式锁:如果公司有久经考验相当靠谱的分布式锁方案,并且公司有非常熟悉分布式锁使用的老司机,否则一旦在并发场景出现问题,很难分析并发问题出现的原因;
(2)数据库行级锁:由于我司电商平台只有一个数据库,所以可以放心使用该方案。行级锁有两种使用方式:
笔者将使用条件更新的方式来保证兑换码执行中记录的唯一性,从而保证兑换码生成任务的唯一性。
具体方案:编写一条根据条件进行插入的sql语句,如下:
# 兑换码记录表dh_code_batch中如果不存在状态为DOING的记录,则写入一条生成记录(状态为DOING)
<insert id="saveIfNotExistDoing" parameterType="com.xxx.DhCodeBatch">
INSERT INTO dh_code_batch (`coupon_id`, `num`, `status`)
SELECT #{batch.couponId}, #{batch.num}, 'DOING' FROM DUAL
WHERE NOT EXISTS (SELECT id FROM dh_code_batch WHERE `status` = 'DOING');
</insert>
那么check()方法中对执行中任务记录的唯一校验代码如下:
if(!dhCodeBatchService.saveIfNotExistDoing(dhCodeBatch)) throw new BusinessException("当前有兑换码生成任务正在执行,请稍后再试!");
至此,兑换码生成记录写入并发问题已解决,兑换码生成任务全局串行得到保证。感谢大家的持续关注!
附带兑换码生成工具类下载!