当前位置: 首页 > article >正文

Java后端开发 ”Bug“ 分享——订单与优惠卷

“优惠券风波”:一段代码引发的线上事故

起因:优惠券功能上线

故事的开始源于公司新上线的一项促销活动——在用户未使用优惠券时,系统会自动赠送一张优惠券。这个功能不仅能提升用户体验,还能拉动平台的销售额。为了赶上活动上线时间,组长将这项任务交给了刚转正的开发小张。

小张心中既紧张又兴奋,立刻投入工作。经过一夜的奋战,当晚,他就提交了代码,并信心满满地对组长说:“放心吧,功能已经写好,测试通过,没有任何问题!”小张对自己的代码很满意,认为加锁和状态校验都已经足够周全。在他的测试环境里,功能表现一切正常,组长也批准了代码上线。

转折:生产环境的“优惠券雨”

功能上线后,活动初期数据非常亮眼——大量用户在收到优惠券后进行了消费。然而,短短几个小时后,运营部却收到了大量用户的投诉:有用户发现自己的账户里收到的优惠券数量成倍增长.

  • 有些用户账户里多出了数十张甚至上百张优惠券。
  • 数据库的CPU使用率飙升,写操作排队严重,导致整个系统几乎瘫痪。
  • 线上出现多起订单错误日志,运维团队忙得焦头烂额。

与此同时,后台的服务压力剧增,数据库的CPU使用率飙升,监控系统发出了一片告警声。

运维火速介入,更严重的是,因为系统的故障,公司损失了几笔大额订单,高层震怒,扬言如果问题不能解决,整个团队都可能面临解散的危机!

话不多说,直接上代码:

    public void getCouponsByOrder(String orderId) {
        //获取订单
        Order order = orderService.getOrderById(orderId);
        if(order == null){
            return ;
        }
        //加锁
        lock.lock();
        order = getOrderForUpdate(orderId);
        try{
            //判断订单是否有优惠券
            if(!order.getHasCoupons()){
                //如果没有使用优惠卷,系统默认赠送优惠卷
                couponsService.createCouponsAndSave(orderId);
            }
        }finally {
            lock.unlock();
        }
    }
危机:组内的混乱排查

团队进入了“战斗模式”。大家围绕这段代码进行分析,但一时半会儿谁也看不出问题。
“逻辑没问题啊,加了锁的,这不就是标准的并发处理方式吗?”小王一脸茫然。
“是不是数据库问题?或者是缓存同步有问题?”测试工程师提出猜测。
“这种锁到底管不管用?”运维开始怀疑架构本身。
大家讨论了整整两天,依然没有找到根本原因。

转机:老员工的登场

无奈之下,组长拨通了一个“传说中”的号码。这是团队里已经调岗的资深工程师老李,他曾是系统的核心开发者,对架构的每个细节了如指掌。

老李接到电话时,正在家中喂猫。听完情况后,他轻轻一笑:“这事儿,听起来有点意思。把代码发我看看。”

老李摇摇头:“这代码确实像个实习生写的,不过问题也不复杂,改改就行了。”

1. MySQL事务默认隔离级别
MySQL默认隔离级别:可重复读 (REPEATABLE READ)

MySQL默认的事务隔离级别是 可重复读 (REPEATABLE READ)。在这种隔离级别下,事务开始后,事务内的所有查询在同一事务中多次读取数据时,结果是一致的,即使其他事务对该数据进行了修改。为了实现这一点,MySQL通过 MVCC(多版本并发控制) 实现快照读。

问题分析:逻辑与隔离级别的交互

代码中的逻辑和隔离级别产生了以下几个潜在问题:

  1. 快照读导致状态不一致
    在调用 orderService.getOrderById(orderId) 时,读取的是快照数据。如果这时有其他事务更新了订单的 hasCoupons 状态,这些修改不会被当前事务看到。
  2. 加锁不生效
    MySQL的SELECT默认是快照读,只有显式使用 FOR UPDATE 或类似语法才能触发行锁。如果没有正确使用锁,即使在事务中操作,订单状态的并发修改也无法避免。
  3. 重复赠送优惠券
    当多个事务几乎同时执行,读取 hasCoupons 时可能都为 false,并发调用了 createCouponsAndSave(orderId),最终导致用户多次收到优惠券。
总的来说就是,当线程A,B进入事务时,生成了此刻的快照,然后A先获取到锁,A修改了数据,但是B此时已经生成了快照,所以B后面拿到的是之前的旧数据。
如何从隔离级别下手解决?

以下是从隔离级别与事务设计入手的解决方案:


改进方案

  1. 将数据库的隔离级别更改成读已提交,即可完美解决

    读已提交是在每一次select的时候生成快照

  2. 使用分布式锁。


http://www.kler.cn/a/453887.html

相关文章:

  • RestTemplate关于https的使用详解
  • 游戏开发-UE4高清虚幻引擎教程
  • 【Git】—— 使用git操作远程仓库(gitee)
  • html + css 淘宝网实战
  • 【路径规划】原理及实现
  • 机器视觉检测相机基础知识 | 颜色 | 光源 | 镜头 | 分辨率 / 精度 / 公差
  • 离心式压缩机设计的自动化方法
  • matlab中的cell
  • 【每日学点鸿蒙知识】类型判断、three.js支持情况、Grid拖动控制、子窗口路由跳转、真机无法断点
  • OpenHarmony 3.2 调用获取指定网络接口信息报错,DHCP报错:callback error 29189
  • 人工智能python快速入门
  • 初始化全部推断的寄存器、 SRL 和存储器
  • 两分钟掌握 TDengine 全部写入方式
  • 目录jangow-01-1.0.1靶机
  • Eclipse常用快捷键详解
  • 【3.1 以太网RDMA优化--网卡缓存资源维度】
  • Android--java实现手机亮度控制
  • react高阶组件及hooks
  • 透视网络世界:计算机网络习题的深度解析与总结【前3章】
  • 物联网乐鑫USB方案,设备互联和数据传输应用
  • Oracle 普通表至分区表的分区交换
  • chrome缓存机制以及验证缓存机制
  • springboot/ssm图书大厦图书管理系统Java代码编写web图书借阅项目
  • uniapp抖音小程序,如何一键获取用户手机号
  • ES学习module模块化(十二)
  • 新建一个springboot项目