一般我们都是这样做的:

创建订单的时候,用订单信息计算一个哈希值,判断redis中是否有key,有则不允许重复提交,没有则生成一个新key,放到redis中设置个过期时间,然后创建订单。其实就是在一段时间内不可重复相同的操作


利用redis保证订单编号不能重复 redis防止订单重复提交_java

第二种方式:利用唯一索引机制的验证

需要原子性操作,想到了数据库的唯一索引。新建一个TradeLock表:

CREATE TABLE `TradeLock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`type` int(11) NOT NULL COMMENT '锁类型',
`lockId` int(11) NOT NULL DEFAULT '0' COMMENT '业务ID',
`status` int(11) NOT NULL DEFAULT '0' COMMENT '锁状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='Trade锁机制';

● 每次request进来则往表里面插入数据:

成功,则可以继续操作(相当于获取锁);

失败,则说明有操作在进行。

● 操作完成后,删除此条记录。(相当于释放锁)。

第二种方法(利用数据库完整性约束)最简便,但是会访问(读写)数据库,给数据库造成一定的压力;
同时也有个隐患,程序执行中途故障了(网络垮了,服务宕了...),后面重复提交,就无法成功了

也有解决方法:定时器清理这个hash数据库表

第三种方式:利用redis的setNX

第四种方式:利用redis的分布式锁(同上setNx命令

  1. set命令
  2. Redission框架)

如果是防重设计,流程图要改改:

利用redis保证订单编号不能重复 redis防止订单重复提交_数据库_02

利用AOP+Redis实现案例

1.自定义注解

/**
 * @author Tzeao
 */
@Target(ElementType.METHOD) // 作用到方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface NoRepeatSubmit {

    //名称,如果不给就是要默认的
    String name() default "name";
}

2.使用AOP实现该注解

/**
 * @author Tzeao
 */
@Aspect
@Component
@Slf4j
public class NoRepeatSubmitAop {

    @Autowired
    private RedisService redisService;

    /**
     * 切入点
     */
    @Pointcut("@annotation(com.qwt.part_time_admin_api.common.validation.NoRepeatSubmit)")
    public void pt() {
    }

    @Around("pt()")
    public Object arround(ProceedingJoinPoint joinPoint) throws Throwable {

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        //这里是唯一标识 根据情况而定
        String key = "1" + "-" + request.getServletPath();
        // 如果缓存中有这个url视为重复提交
        if (!redisService.haskey(key)) {
            //通过,执行下一步
            Object o = joinPoint.proceed();
            //然后存入redis 并且设置15s倒计时
            redisService.setCacheObject(key, 0, 15, TimeUnit.SECONDS);
            //返回结果
            return o;
        } else {
            return Result.fail(400, "请勿重复提交或者操作过于频繁!");
        }

    }
}

3、测试

@NoRepeatSubmit(name = "test") // 也可以不给名字,这样就会走默认名字
    @GetMapping("test")
    public Result test() {
        return Result.success("测试阶段!");
    }

悲观锁和乐观锁

具体步骤:

先根据id查询用户信息,包含version字段

根据id和version字段值作为where条件的参数,更新用户信息,同时version+1

判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。

如果影响0行,说明是重复请求,则直接返回成功。

在更新数据之前先查询一下数据:

select id,amount,version from user id=123;

如果数据存在,假设查到的version等于1,再使用idversion字段作为查询条件更新数据:

update user set amount=amount+100,version=version+1
where id=123 and version=1;

更新数据的同时version+1,然后判断本次update操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。

由于第一次请求version等于1是可以成功的,操作成功后version变成2了。这时如果并发的请求过来,再执行相同的sql:

update user set amount=amount+100,version=version+1
where id=123 and version=1;

update操作不会真正更新数据,最终sql的执行结果影响行数是0,因为version已经变成2了,where中的version=1肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为version值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

具体流程如下:



利用redis保证订单编号不能重复 redis防止订单重复提交_redis_03

对于创建订单和更新订单

创建订单服务,可通过预生成订单号,然后利用DB的订单号唯一约束,避免重复写入订单,实现创建订单服务的幂等性

更新订单服务,通过一个版本号机制,每次更新数据前校验版本号,更新数据同时自增版本号,这样的方式,来解决ABA问题,确保更新订单服务的幂等性

总结

对于重复提交请求的问题,我们单纯的只从前端或后端控制,带来的用户体验都不是最好的。只有两者结合起来,才能在确保功能正常的前提下,保证用户体验效果。

PS:如果想学习技术,或者在学习技术的过程中有疑问,对编程方向的选择,可以来这里找小于哥,一个有思想有规划,被代码延误的心灵导师,可咨询offer的选择,职业规划,学习路线,技术开发中的问题

我是终端研发部的小于哥