幂等 2022-12-08 13:10 ### 什么是幂等性? 对于同一个请求,不管调用多少次,对于资源本身应该具有同样的结果。 幂等性是为了解决重复提交问题,比如:恶意刷单,重复支付等。有时网络波动也有可能提交两次同样的请求过来。 ### 解决方案: 1、乐观锁,常用在数据库中,更新数据时根据版本号或状态码更新,只能更新成功一次。 2、唯一序列号,请求时携带唯一序列号。MySQL中建立唯一约束,插入时成功则处理完成,失败则回滚事务。 不管用乐观锁还是唯一序列号, 大致步骤都差不多,最后重点是将可能出问题的操做包在事务里,最后用一个互斥的动作保证只执行成功一次。成功就提交事务;失败就回滚事务,这样出问题的操作也就能回滚了。 ①检查,处理过,直接返回have Completed;未处理,向下走 ②开启事务 ③可能出问题的操作 ④利用乐观锁/唯一序列号进行互斥操作 ⑤成功提交事务,失败回滚事务 最后,只要前面的检查通过了,向下走,后面不管是否成功提交事务,都返回Completed。 ### 案例 > https://mp.weixin.qq.com/s/46b_jVU1G98ThplvB62H9g 对接支付宝充值时,要提供给支付宝一个回调接口,回调时会携带(out_trade_no【商户订单号】,trade_no【支付宝交易号】),trade_no在支付宝中是唯一的。这个回调接口要保证幂等,即:使用同样的请求参数多次调用这个接口,结果是一样的(跟调用一次没有区别)。 以下解决方案,核心是利用数据库回滚事务来保证只有一个成功。 ```sql # 订单表储存了用户Id和订单号以及订单状态,订单号就是trade_no CREATE TABLE t_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键,自动增长', user_id BIGINT NOT NULL COMMENT '用户id', order_id VARCHAR(255) NOT NULL DEFAULT '' COMMENT '订单号', status INT NOT NULL DEFAULT 0 COMMENT '订单状态' ) COMMENT '订单表'; # 支付宝回调我们说明用户充值了,那我们要给我们系统中的用户账户加钱。账户表储存了用户id和余额 CREATE TABLE t_account ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键,自动增长', user_id BIGINT NOT NULL COMMENT '用户id', account DECIMAL(13,3) DEFAULT 0 COMMENT '用户余额' ) COMMENT '账户表'; ``` #### 乐观锁: 1.接收到支付宝支付成功请求 2.查询订单信息 ```sql select * from t_order where order_id = trade_no; ``` 3.判断订单是否已处理(status) 4.如果订单已处理直接返回,若未处理,继续向下执行 5.打开本地事务:`start transaction` 6.给本地系统给用户加钱: ```sql update t_account set account = account + 100 where user_id = ? ``` 7.将订单状态置为成功,(注意这块是重点)伪代码: ```sql update t_order set status = 1 where order_id = trade_no and status = 0; //上面的update操作会返回影响的行数num if(num==1){ //表示更新成功 提交事务;前面加的钱成功 }else{ //表示更新失败 回滚事务;前面加的钱取消 } ``` 即使有两个同样的请求同时过来,都走到第7步了,由于update时操作同一条记录,所以都会加行排他锁,互斥,最终只会有一个线程执行成功,其他的线程阻塞等待执行成功的线程执行完之后,就会更新失败。失败后就会回滚事务,将上面给本地系统加的钱去掉。(不管是版本号还是状态码之类的,利用这个做的其实不太应该叫乐观锁,它表面上乐观,但用的是数据库的行锁,是一种悲观锁。) 最终只有一个请求能成功加钱。 ##### 代码: https://github.com/hanhanhanxu/Idempotent ##### 压测 ab -n 50 -c 50 -p 123.txt -T application/json -k http://localhost:8080/api/recharge 123.txt: { "tradeNo":"123" } #### 唯一约束: 创建一个交易流水表,储存交易类型和对应的订单号: ```sql CREATE TABLE `t_uq_dipose` ( `id` bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY, `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '关联对象类型', `ref_id` varchar(255) NOT NULL DEFAULT '' COMMENT '关联对象id', UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保证业务唯一性' ); ``` 当有交易发生时,插入数据,利用`ref_type`,`ref_id`的联合唯一性,保证一个操作只成功一次。 1.接收到支付宝支付成功请求 2.查询t_uq_dipose(条件ref_id,ref_type),可以判断订单是否已处理 ``` select * from t_uq_dipose where ref_type = '充值订单' and ref_id = trade_no; ``` 3.判断订单是已处理 4.如果订单已处理直接返回,若未处理,继续向下执行 5.打开本地事务:`start transaction` 6.给本地系统里的用户加钱 ```sql update t_account set account = account + 100 where user_id = ? ``` 7.将订单状态置为成功 ```sql update t_order set status = 1 where order_id = trade_no; ``` 8.向t_uq_dipose插入数据,插入成功,提交本地事务,插入失败,回滚本地事务,伪代码: ```sql try{ insert into t_uq_dipose (ref_type,ref_id) values ('充值订单',trade_no); //提交本地事务: }catch(Exception e){ //回滚本地事务; } ``` --END--
发表评论