最近项目里碰到关于水电煤缴费 出现了账户余额剩余100元,但同一时间可以成功支付4笔100元订单的问题.出现这种问题的原因主要在于短时间内前端按钮操作抖动多次请求的情况.同时后端在判断账户余额然后下单的过程中出现了多线程并发的问题。解决这种问题的方法一般是从解决订单重复提交+防止高并发情况下账户余额为负数的情况解决方案。(类似于商品库存超卖的解决方案)
一般完整的订单支付流程是这样的
1.后端生成订单编号,前端将订单编号填充的下单页面中
2.下单页面 确认订单编号对应的下单相关信息,提交给后端
3.后端首先判断该笔订单编号是否提交,未提交判断账户余额信息是否够用,如果够用下单,不够用提示前端。如果已提交 提示订单重复提交.
关键在于同一时间下单4笔100元,判断账户余额到下单修改账户余额存在间隔,只要在此时间段过来的请求都可能发生下单多笔成功但实际账户余额不足以支付的问题.
防止订单重复提交的技术解决方案
防止订单重复提交 参考文章链接 关于防止订单重复提交的技术解决方案这里面做了很好的介绍,包含4中解决方案,但前面2种方案因为查询订单到插入订单存在间隔,无论是采用mysql还是缓存的方式都会存在问题。所以这里只把采用唯一索引表+redis计数器的方案使用说明下。因为无论是查询订单是否重复提交的判断是基于数据库或者缓存是否存在该笔订单编号,如果存在不存在说明没有重复,插入数据,存在说明已重复无法提交.所以在查询数据到修改数据库中间会存在间隔,会出现问题。所以需要利用原子性.如果想更加保险的话,建议通过token令牌的方式,后台创建订单好的同时创建缓存中,新增的时候判断订单是否存在,存在才可以提交订单,逻辑判断完毕删除该订单.后面的相同的订单因为缓存中不存在该订单号,所以会有问题.
- 创建唯一索引表:创建根据订单编号来插入的唯一索引表。不存在插入成功说明未重复,存在说明重复。无法插入 报错.
- redis计数器:订单进来创建该笔订单的唯一编号计数器+1.如果大于1说明重复,等于1说明不重复.
实际例子说明
这里我创建了接口,订单编号传过来我就+1,然后判断订单编号的数值是否大于1,如果大于1说明重复,等于1说明不重复.主要是用了redis中计数器原子特性.也就是说上一个操作在完成之前不能有新的操作进来.上例子
@Controller
@RequestMapping("/test")
public class TestController extends BaseController {
`@RequestMapping("/send")
@ResponseBody
public String send(String id) {
redisTemplate.opsForValue().increment(id, 1);//订单编号提交redis计数器+1
//获取计数器是多少
String num=(String) redisTemplate.opsForValue().get(id);
logger.info(num);
//如果计数器不等于 说明订单重复提交 其实就是大于 1的情况
if(!"1".equals(num)){
throw new MessageException("订单重复提交");
}
return "hello";
}
Jemeter测试高并发条件下 订单是否会重复提交
可以看到非高并发情况下存在先后顺序的情况id=004是未重复返回hello,重复的话返回订单重复提交的提示的.下面我们来测试一下1000个线程该并发的情况下的情况。下面测试下id=005.高并发500次的情况.
上述的结果看到即使在500个并发情况能够保证同一笔单子 没有出现重复提交的情况,配合上Token令牌效果更佳.
通过唯一索引表 防止订单重复提交(但是效率不如redis计数器的方式)
@Controller
@RequestMapping("/test")
public class TestController extends BaseController {
`@RequestMapping("/send")
@ResponseBody
public String send(String id) {
//通过唯一索引机制 防止订单重复提交
SysPayLockCallback sysPayLockCallback=new SysPayLockCallback();
sysPayLockCallback.setOpenid(id);
try{
sysPayLockMapper.insertSelective(sysPayLockCallback);
}catch(Exception e) {//如果唯一索引表 插入失败 订单重复提交
throw new MessageException("订单重复提交");
}
return "hello";
}
同样这种方式也可以解决订单重复提交的问题.
总结来看
防止订单重复提交的方式多种多样,主要在于了解市面上主要防止订单重福提交的方案有哪些,有哪些问题及不足。是否适合当前的业务场景等,毕竟技术是服务于业务的。比如Token令牌机制 后端生成订单号并放入缓存,前端提交该订单信息时,后端接口判断是否存在,不存在 说明是首次,删除缓存订单.存在说明是重复的。这种方式对于要求 不高的业务场景是没有问题的,如不涉及支付的,因为在高并发情况下,判断缓存订单到删除的过程中同样可能会有其他线程同样判断了订单不存在 执行下面的业务流导致问题,并不适用于现在的支付场。所以必须使用可以利用原子特性的避免这种查询和修改可以并行情况的发生,如redis计数器及唯一索引机制 .这样可以保证在多个线程在查询状态的时候不会处在其他数据处在修改的阶段.