一般我们都是这样做的:
创建订单的时候,用订单信息计算一个哈希值,判断redis中是否有key,有则不允许重复提交,没有则生成一个新key,放到redis中设置个过期时间,然后创建订单。其实就是在一段时间内不可重复相同的操作
第二种方式:利用唯一索引机制的验证
需要原子性操作,想到了数据库的唯一索引。新建一个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命令
- set命令
- Redission框架)
如果是防重设计,流程图要改改:
利用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
,再使用id
和version
字段作为查询条件更新数据:
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
值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。
具体流程如下:
对于创建订单和更新订单
创建订单服务,可通过预生成订单号,然后利用DB的订单号唯一约束,避免重复写入订单,实现创建订单服务的幂等性
更新订单服务,通过一个版本号机制,每次更新数据前校验版本号,更新数据同时自增版本号,这样的方式,来解决ABA问题,确保更新订单服务的幂等性
总结
对于重复提交请求的问题,我们单纯的只从前端或后端控制,带来的用户体验都不是最好的。只有两者结合起来,才能在确保功能正常的前提下,保证用户体验效果。
PS:如果想学习技术,或者在学习技术的过程中有疑问,对编程方向的选择,可以来这里找小于哥,一个有思想有规划,被代码延误的心灵导师,可咨询offer的选择,职业规划,学习路线,技术开发中的问题
我是终端研发部的小于哥