文章收录在我的 GitHub 仓库,欢迎Star/fork:
Java-Interview-Tutorial https://github.com/Wasabi1234/Java-Interview-Tutorial
可能你最先想到的就是使用数据库的事务保证。比如创建订单时,要同时往订单表和订单商品表中插入数据,那这些插入数据的INSERT必须在一个数据库事务中执行,数据库的事务可以确保:执行这些INSERT语句,共赴生死!
假如你有个服务部署在5台机器上,有个付款接口。然后用户在前端操作时,一份订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,结果一个订单扣款扣两次,gg!
订单系统调用支付系统进行支付,结果不小心网络超时,然后订单系统走了前面我们看到的那个重试机制,给你重试了一把,好,支付系统收到一个支付请求两次,而且因为负载均衡算法落在了不同的机器了!
但这还是有很多大坑存在。一个分布式系统中的某个接口,要保证幂等性,如何保证?
评论里有同学说,前端页面直接防止用户重复提交表单。没啥毛病,但网络错误会导致重传,很多RPC框架、网关都有自动重试机制,所以重复请求无法避免。
所以问题归结于如何保证服务接口的幂等性。
保证幂等性主要有如下几点
比如订单支付请求,得包含订单id,一个订单id最多支付一次
比如说常见的方案是在MySQL中记录一个状态字段。比如支付之前记录一条这个订单的支付流水
若有一个订单已支付,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId已存在,唯一键约束生效,报错插入不进去。就不会再重复扣款。
在往db插条记录时,一般不提供主键,而由数据库在插入时自动生成一个主键。这样重复的请求就会导致插入重复数据。
MySQL的主键自带唯一性约束,若在一条INSERT语句提供主键,且该主键值在表中已存在,则该条INSERT会执行失败。因此可利用db的“主键唯一约束”,在插数据时带上主键,以此实现创建订单接口的幂等性。
给订单服务添加一个“orderId生成”的接口,无参,返回值就是一个全局唯一订单号。在用户进入创建订单页面时,前端页面先调用该orderId生成接口得到一个订单号,在用户提交订单的时候,在创建订单的请求中携带该订单号。
该订单号其实就是订单表的主键,如此一来,重复请求中带的都是同一订单号。订单服务在订单表中插入数据的时候,执行的这些重复INSERT语句中的主键,也都是同一个订单号。而数据库的唯一约束可保证,只有一次INSERT执行成功。
实际要结合业务,比如使用Redis,用orderId作为唯一键。只有成功插入这个支付流水,才可执行扣款。
要求是支付一个订单,必须插入一条支付流水,order_id建立一个唯一键unique key
你在支付一个订单前,先插入一条支付流水,order_id
就已经传过去了
你就可以写一个标识到Redis中,set order_id payed
,当重复请求过来时,先查Redis的order_id
对应的value
,若为payed
说明已支付,就别重复支付了!
然后再重复支付订单时,写尝试插入一条支付流水,db会报错unique key冲突,整个事务回滚即可。
保存一个是否处理过的标识也可以,服务的不同实例可以一起操作Redis。
如果因为重复订单导致插入订单表失败,订单服务不要把这个错误返回给前端页面.
否则,就可能出现用户点击创建订单按钮后,页面提示创建订单失败,而实际上订单却创建成功了.
正确的做法是,遇到这种情况,订单服务直接返回订单创建成功即可.
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。