优惠券兑换码生成需求——事务失效问题分析

前段时间收到一个优惠券兑换码的需求:管理后台针对一个优惠券发起批量生成兑换码,这些兑换码可以导出分发到各个合作渠道(比如:抖音、京东等),用户通过这些渠道获取到兑换码之后,再登录到我司研发的商城,使用兑换码兑换获得对应的优惠券。

整个需求大致分为两个部分:(1)批量生成兑换码;(2)使用兑换码兑换优惠券。接下来的几篇文章将针对批量生成兑换码功能实现过程中碰到的一系列问题进行分析描述,以便读者再碰到类似问题,可以快速解决。

文章系列如下:

《事务失效问题分析》

《事务同步回调问题分析》

《批量生成任务全局限制唯一》

《方案优化:批量插入、批量导出、异步+补偿》

在此之前,先简单介绍商城技术架构:商城后端服务均采用SpringCloud框架开发,数据库主备,商城所有服务共用一个数据库,数据库持久化框架为MybatisPlus,所有服务采用K8s技术进行部署和治理。


一、问题描述

言归正传,看到需求首先想到的是:一个兑换码兑换一张优惠券,要保证兑换码生成数量的准确性,不能生成少了,也不能多了。因为优惠券既可以被用户领取,也可以生成兑换码分发出去,就会出现一种并发情况:后台一边生成兑换码,用户一边通过平台各种活动领取对应的优惠券。优惠券的可用数量受到这两个情况的影响发生变化。基于以上情况考虑,兑换码生成过程需要进行事务控制。下面是兑换码生成的简略代码:

public class DhCodeController {

    @Resource
    private DhCodeService dhCodeService;
    
    @PostMapping("/create")
    public R<Boolean> create(@RequestBody @Valid CodeCreateReqDTO codeCreateReqDTO) {
        dhCodeService.create(codeCreateReqDTO);
        return R.ok(Boolean.TRUE);
    }
}

public interface DhCodeService extends IService<DhCode> {
   /**
    * 生成兑换码
    * @param codeCreateReqDTO
    * @return
    */
   void create(CodeCreateReqDTO codeCreateReqDTO);
   /**
    * 实际生成兑换码处理
    * @param codeCreateReqDTO
    * @return
    */
   void doCreate(CodeCreateReqDTO codeCreateReqDTO);
}

@Service
@Slf4j
public class DhCodeServiceImpl extends ServiceImpl<DhCodeMapper, DhCode> implements DhCodeService {
    @Override
    public void create(CodeCreateReqDTO codeCreateReqDTO) {
        //重复提交拦截
         ...
        //校验剩余可生成数量
        ...
        // 如果优惠券剩余数量 - 未使用兑换码数量 < 本次兑换码生成数量,则抛出异常
        ...
        //批量创建兑换码
        doCreate(redeemCodeCreateReqDTO);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void doCreate(CodeCreateReqDTO codeCreateReqDTO) {
        StopWatch stopWatch = new StopWatch("兑换码生成");
        
        DhCodeBatch dhCodeBatch = new DhCodeBatch();
        ...
        //写入本次兑换码生成记录
        if(dhCodeBatchService.save(dhCodeBatch)) {
            //生成兑换码并批量入库
            stopWatch.start("生成随机code");
            List<String> codeList = RedeemCodeUtils.generateRedeemCodes(codeCreateReqDTO.getNumber());
            stopWatch.stop();
            
            stopWatch.start("构建对象列表");
            List<DhCode> dhCodeList = codeList.stream().map(s -> {
                 DhCode dhCode = new DhCode();
                 ...
                 return dhCode;
            }).collect(Collectors.toList());
            stopWatch.stop();
            
            stopWatch.start("批量写入");
            if(!dhCodeService.saveBatch(dhCodeList)) throw new BusinessException(CommonConstants.FAIL, "批量保存兑换码失败!");
            stopWatch.stop();
            
            stopWatch.start("更新数量和状态");
            //更新优惠券已生成兑换码数量和未兑换的兑换码数量, 更新兑换码生成记录状态
            if(updateDhCodeNumberAndGenerateStatus(...))) {
                 log.info("优惠券生成兑换码成功!");
            } else {
                 throw new BusinessException(CommonConstants.FAIL, "更新优惠券兑换码数量或兑换码记录状态失败!");
            }
            stopWatch.stop();
        } else {
            throw new BusinessException(CommonConstants.FAIL, "保存兑换码生成记录失败!");
        }
        log.info(stopWatch.prettyPrint(TimeUnit.SECONDS));
    }
}

上述代码中doCreate方法负责实际批量生成兑换码并写入数据,如果执行过程中出现异常,则进行回滚。但是在实际接口调用中,该方法未实现事务回滚,并且数据出现部分提交。由此可知,该方法标注的@Transactional(rollbackFor = Exception.class)未生效。

二、问题分析

一般什么情况会导致事务失效呢?借鉴同行总结的结果(具体信息请移步至本文第四节【参考资料】),如下图所示:

结合上图和代码,细心的读者可能很快就能发现,笔者的代码命中了第三点——方法内部调用。如果一个加了事务控制的方法被同类中的其他方法调用,则事务控制失效。

三、解决方案

针对上述事务控制失效的场景,笔者采用的方法是:调整代码结构,避免事务控制的方法在同类中方法调用。

在兑换码生成功能中,我将create方法实现的逻辑分为两个方法:check和create(删除现在的create方法,将doCreate改名为create方法),然后在Controller中直接调用check和create方法。调整后代码结构如下:

public class DhCodeController {

    @Resource
    private DhCodeService dhCodeService;
    
    @PostMapping("/create")
    public R<Boolean> create(@RequestBody @Valid CodeCreateReqDTO codeCreateReqDTO) {
        if(dhCodeService.check(codeCreateReqDTO)) {
            dhCodeService.create(codeCreateReqDTO);
            return R.ok("兑换码生成成功!");            
        }
        return R.fail("参数校验错误,兑换码生成失败!");
    }
}

public interface DhCodeService extends IService<DhCode> {
   /**
    * 生成兑换码参数校验
    * @param codeCreateReqDTO
    * @return
    */
   boolean check(CodeCreateReqDTO codeCreateReqDTO);
   /**
    * 生成兑换码处理
    * @param codeCreateReqDTO
    * @return
    */
   void create(CodeCreateReqDTO codeCreateReqDTO);
}

@Service
@Slf4j
public class DhCodeServiceImpl extends ServiceImpl<DhCodeMapper, DhCode> implements DhCodeService {
    @Override
    public boolean check(CodeCreateReqDTO codeCreateReqDTO) {
        //重复提交拦截
         ...
        //校验剩余可生成数量
        ...
        // 如果优惠券剩余数量 - 未使用兑换码数量 < 本次兑换码生成数量,则抛出异常
        ...
        // 校验是否有生成中的兑换码任务,没有则写入一条,有则抛出异常
        
        return Boolean.TRUE;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void create(CodeCreateReqDTO codeCreateReqDTO) {
        StopWatch stopWatch = new StopWatch("兑换码生成");
        
        //生成兑换码并批量入库
        stopWatch.start("生成随机code");
        List<String> codeList = RedeemCodeUtils.generateRedeemCodes(codeCreateReqDTO.getNumber());
        stopWatch.stop();
        
        stopWatch.start("构建对象列表");
        List<DhCode> dhCodeList = codeList.stream().map(s -> {
             DhCode dhCode = new DhCode();
             ...
             return dhCode;
        }).collect(Collectors.toList());
        stopWatch.stop();
        
        stopWatch.start("批量写入");
        if(!dhCodeService.saveBatch(dhCodeList)) throw new BusinessException(CommonConstants.FAIL, "批量保存兑换码失败!");
        stopWatch.stop();
        
        stopWatch.start("更新数量和状态");
        //更新优惠券已生成兑换码数量和未兑换的兑换码数量, 更新兑换码生成记录状态
        if(updateDhCodeNumberAndGenerateStatus(...))) {
             log.info("优惠券生成兑换码成功!");
        } else {
             throw new BusinessException(CommonConstants.FAIL, "更新优惠券兑换码数量或兑换码记录状态失败!");
        }
        stopWatch.stop();
  
        log.info(stopWatch.prettyPrint(TimeUnit.SECONDS));
    }
}

代码经过调整之后,Controller中的接口方法直接调用create方法,实现事务控制。

但是这里有一个问题:还记得在批量生成兑换码之前写入数据库的生成记录吗?默认状态是生成中,如果事务成功提交,则状态被更新为成功。如果执行异常,事务回滚,这个状态值会变吗?不会变!

如果执行异常,事务回滚,状态更新为失败,怎么办呢?请读者继续阅读《事务同步回调问题分析》。

四、参考资料

  1. 兑换码生成工具类下载
  2. spring 事务失效的 12 种场景​​​​​​

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/321199.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

将Android应用修改为鸿蒙应用的工作

将Android应用修改为鸿蒙&#xff08;HarmonyOS&#xff09;应用需要进行一系列主要的工作。以下是在进行这一转换过程中可能需要进行的主要工作&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.项目…

Vue2x的自定义指令和render函数使用自定义指令

在某些情况下&#xff0c;我们需要对底层DOM进行操作&#xff0c;而内置的指令不能满足需求&#xff0c;就需要自定义指令。一个自定义指令由一个包含类似组件的生命周期的钩子的对象来定义&#xff0c;钩子函数会接收到指令所绑定的元素作为参数。 定义指令 常用两种方式进行…

【天龙怀旧服】攻略day7

关键字&#xff1a; 新星1.49、金针渡劫、10灵 1】新星&#xff08;苍山破煞&#xff09; 周三周六限定副本&#xff0c;19.00-24.00 通常刷1.49w&#xff0c;刷149点元佑碎金 boss选择通常为狂鬼难度&#xff0c;八风不动即放大不选&#xff0c;第二排第一个也不选&#xf…

SAP SD-DN-MM 交货单相关物料凭证的视图的日期问题

眼下有个需求 获取交货单对应的物料凭证的过账日期BLDAT。 同步BW数据过去 新增一个数据库视图 但是实际使用时&#xff0c;有效部分仅本月&#xff0c;再选择条件里面要加上 MATdoc-bldat > sy-datum - sydatum6(2). 于是使用ST05 跟踪了一下&#xff0c;发现在DD28S…

算法通关村第十五关—继续研究超大规模数据场景的问题(黄金)

继续研究超大规模数据场景的问题 一、对20GB文件进行排序 题目要求&#xff1a;假设你有一个20GB的文件&#xff0c;每行一个字符串&#xff0c;请说明如何对这个文件进行排序&#xff1f;  分析&#xff1a;这里给出大小是20GB,其实面试官就在暗示你不要将所有的文件都装入到…

【ROS2】使用C++实现简单的发布订阅方

1 构建自定义数据类型 1、自定义消息类型Student 1.1 创建base_interfaces_demo包 1.2 创建Student.msg文件 string name int32 age float64 height 1.2 在cmakeLists.txt中增加如下语句 #增加自定义消息类型的依赖 find_package(rosidl_default_generators REQUIRED) # 为…

Pytorch基础知识点复习

文章目录 并行计算单卡训练多卡训练单机多卡DP多机多卡DDPDP 与 DDP 的优缺点 PyTorch的主要组成模块Pytorch的主要组成模块包括那些呢&#xff1f;Dataset和DataLoader的作用是什么&#xff0c;我们如何构建自己的Dataset和DataLoader&#xff1f;神经网络的一般构造方法&…

机器学习~从入门到精通(三)梯度下降法

一、梯度下降法 # 梯度下降不是一种算法&#xff0c;是一种最优化方法 # 上节课讲解的梯度下降的案例 是一个简单的一元二次方程 # 最简单的线性回归&#xff1a;只有一个特征的线性回归&#xff0c;有两个theta # 二、在多元线性回归中使用梯度下降求解 三、### R…

泊松流生成模型简介

一、说明 泊松流生成模型 (PFGM) 是一种新型的生成深度学习模型&#xff0c;与扩散模型类似&#xff0c;其灵感来自物理学。在这本简单易懂的指南中了解 PFGM 背后的理论以及如何使用它们生成图像。 生成式人工智能模型在过去几年中取得了长足的进步。受物理启发的扩散…

使用pygame实现简单的烟花效果

import pygame import sys import random import math# 初始化 Pygame pygame.init()# 设置窗口大小 width, height 800, 600 screen pygame.display.set_mode((width, height)) pygame.display.set_caption("Fireworks Explosion")# 定义颜色 black (0, 0, 0) wh…

BLDC 电机和 PMSM 的结构区别

BLDC 电机和 PMSM 的结构类似&#xff0c;其永磁体均置于转子&#xff0c;并被定义为同步电机。在同步电机中&#xff0c;转子与定子磁场同步&#xff0c;即转子的旋转速度与定子磁场相同。它们的主要区别在于其反电动势&#xff08;反 EMF&#xff09;的形状。电机在旋转时充当…

MySQL进阶篇(五) 锁

一、概述 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中&#xff0c;除传统的计算资源&#xff08;CPU、RAM、I/O&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问…

基于JavaWeb的酒店管理系统

基于JavaWeb的酒店管理系统 文章目录 基于JavaWeb的酒店管理系统系统介绍技术选型成果展示源码获取账号地址及其他说明 系统介绍 基于JavaWeb的酒店管理系统是为酒店打造的管理平台&#xff0c;其主要功能有管理员登陆、客房预订、客房入住、房间管理、数据查询(预订单查询、入…

ros2 基础学习 15- URDF:机器人建模方法

URDF&#xff1a;机器人建模方法 ROS是机器人操作系统&#xff0c;当然要给机器人使用啦&#xff0c;不过在使用之前&#xff0c;还得让ROS认识下我们使用的机器人&#xff0c;如何把一个机器人介绍给ROS呢&#xff1f; 为此&#xff0c;ROS专门提供了一种机器人建模方法——…

数据结构.线性表(2)

一、模板 例子&#xff1a; a: b: 二、基本操作的实现 &#xff08;1&#xff09;初始化 &#xff08;2&#xff09;销毁和清空 &#xff08;3&#xff09;求长度和判断是否为空 &#xff08;4&#xff09;取值 &#xff08;5&#xff09;查找 &#xff08;6&#xff09;插入 &…

【rust/bevy】从game template开始

目录 说在前面步骤进入3D控制方块问题 说在前面 操作系统&#xff1a;win11rust版本&#xff1a;rustc 1.77.0-nightlybevy版本&#xff1a;0.12 步骤 rust安装 这里 windows下建议使用msvc版本bevy安装 这里clone代码git clone https://github.com/NiklasEi/bevy_game_templa…

Jetpack Flow 、Room 初学者学习记录

学习使用响应式Flow操作数据&#xff0c;记录自己学习的过程。 ContactViewModel 是一个 ViewModel&#xff0c;它依赖于一个Room操作接口 ContactDao &#xff0c;访问对象来获取联系人数据。它使用了 StateFlow 来处理状态的变化和数据的更新。ViewModels 通常用于管理应用的…

emc防护原理

emc原理 emc设计的根本是对于干扰电流的阻塞和疏导。通过阻抗变化来实现对于特定频率的电流的防护。 emc设计 在485等信号线emc实验中&#xff0c;一般使用8us/20us的脉冲波形进行浪涌信号线实验即可。在特殊场景&#xff08;军品&#xff09;会使用到更高等级的浪涌实验&am…

在illustrator中按大小尺寸选择物体 <脚本 018>

在Illustrator中我们可以依据对象的属性 如&#xff1a;填充颜色、描边颜色或描边宽度来选择相同属性的对象&#xff0c;但是Illustrator中没有根据不同大小尺寸来选择对象的功能&#xff0c;下面介绍的就是根据大小尺寸选择对象的脚本。 1、下面是当前画板中的所有对象&#…

Linux远程登陆协议ssh

目录 一、SSH服务 1. ssh基础 2. 原理 3. 服务端配置 3.1 常用配置项 3.2 具体操作 3.2.1 修改默认端口号 3.2.2 禁止root用户登录 3.2.3 白名单列表 3.2.4 黑名单列表 3.2.5 使用秘钥对及免交互验证登录 3.2.6 免交互式登录 一、SSH服务 1. ssh基础 SSH&…