并发数据问题
技术是解决问题慢慢出现的,不是凭空设计的。
幂等
定义:
接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的 ;
幂等的实现:
- 数据库UK天然实现,插入时考虑同一个uk时的告警处理,更新时可以使用数据库乐观锁,加version;
- redis实现,指令setnx;
模型示例:
正常创单模型
- 用户创单生成订单落库,此时订单状态位待支付
- 用户支付成功,更改订单状态已支付,履约接单
- 物理域操作拣货-代发货,出库-已发货,货到达快递点-待收货
- 用户收获,交易完成
涉及三方模型
上述创单成功,现在和三方和作,比如现在比较多的店长团长端需要看到用户信息,则订单支付成功后,需要同步调用店长端应用,进行部分可见订单信息的处理,示意如下;
店长域展示
本次设计只是做幂等方面的展示,模型仅仅示例,主要是店长域的订单插入和更新做幂等校验。我们要考虑的核心点是:分布式情况下,接口调用多次,怎么保证插入到店长订单DB的数据不会重复
表设计
表名:leader_order
uk:order_id
名称 类型 可空 注释 id bigint N gmt_creat datetime N 创建时间 gmt_modify datetime N 修改时间 order_id varchar(32) N 订单号,年月日+redis生成的数字,从1开始,不够了补位,24位 order_status int N 订单状态,0-已支付,10-代发货,20-已发货,30-待取货,40-交易完后,80-交易关闭 order_amount varchar(32) N 支付金额 version int N 版本号,默认0
表详细字段示例:
链路示意
更改和插入链路一样,不做赘述
代码展示
数据库层面实现
插入
无uk插入
数据库设计不做订单号唯一uk情况;是会插入相同的数据的;
service
public void insertOrder(OrderStatusRequest request) {
// 模拟并发线程切换
System.out.println(Thread.currentThread().getName() + "开始休眠," + "时间:" + new Date());
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
LeaderOrderVO leaderOrderVO = LeaderOrderConvert.convert2LeaderOrderVO(request);
leaderOrderDAO.insertOrder(leaderOrderVO);
}
sql
<insert id="insertOrder" parameterType="com.blabla.dao.vo.LeaderOrderVO">
insert into leader_order
values (null,now(),now(),#{orderId},#{orderStatus},#{orderAmount},0);
</insert>
代码日志示例
数据库插入数据
增加团长订单表订单id为唯一uk,其实唯一uk已经作了天然幂等;
uk插入
uk示例:
调用示例:
这里两种方案:
-
try-catch掉主键冲突,sql不变,java代码![请添加图片描述](https://img-blog.csdnimg.cn/6b9557faffe9411aa16aae0771fffecf.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Y-W5ZCN6L-Y6KaB5oOz5Y2K5aSp,size_20,color_FFFFFF,t_70,g_se,x_16加try-catch public void insertOrder(OrderStatusRequest request) { // 模拟并发线程切换 System.out.println(Thread.currentThread().getName() + “开始休眠,” + “时间:” + new Date()); try { Thread.sleep(1000 * 10); } catch (InterruptedException e) { e.printStackTrace(); } LeaderOrderVO leaderOrderVO = LeaderOrderConvert.convert2LeaderOrderVO(request); try { leaderOrderDAO.insertOrder(leaderOrderVO); } catch (DuplicateKeyException e) { System.out.println(“主键冲突:” + e.getMessage()); } } 效果: -
插入sql加ignore,进行去重;运行不报错。 insert ignore into leader_order values (null,now(),now(),#{orderId},#{orderStatus},#{orderAmount},0);
- 插入sql,存在就更新,但是对于订单来说,我们希望保存第一手的数据,这里只做sql示意
insert into leader_order value(“xx”,“xx”) ON DUPLICATE KEY UPDATE
更新
数据库乐观锁实现,主要是version关键子
public void updateOrderStatusByOrderId(OrderStatusRequest request) {
Integer version = leaderOrderDAO.getVersionByOrderId(request.getOrderId());
LeaderOrderVO leaderOrderVO = LeaderOrderConvert.convert2LeaderOrderVO(request);
leaderOrderVO.setVersion(version);
System.out.println("获取数据库version:" + version);
// 模拟并发线程切换
System.out.println(Thread.currentThread().getName() + "开始休眠," + "时间:" + new Date());
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
leaderOrderDAO.updateOrderStatus(leaderOrderVO);
}
<update id="updateOrderStatus" parameterType="com.blabla.dao.vo.LeaderOrderVO">
update leader_order
set
gmt_modify = now()
<!--<if test ="LeaderOrderVO.getOrderStatus != null">
,order_status = #{orderStatus}
</if>
<if test ="LeaderOrderVO.getOrderAmount != null">
,order_amount = #{orderAmount}
</if>-->
,version = version+1
where
order_id = #{orderId} and
version = #{version}
</update>
redis实现
setnx 以插入为例
public void insertOrder(OrderStatusRequest request) {
// 模拟并发线程切换
System.out.println(Thread.currentThread().getName() + "开始休眠," + "时间:" + new Date());
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
LeaderOrderVO leaderOrderVO = LeaderOrderConvert.convert2LeaderOrderVO(request);
if (redisUtil.setnxExpire(request.getOrderId(), request.getOrderStatus(), 60L)) {
System.out.println(Thread.currentThread().getName() + "准备执行插入数据库");
leaderOrderDAO.insertOrder(leaderOrderVO);
} else {
System.out.println(Thread.currentThread().getName() + "未执行插入数据库");
}
}
package com.blabla.utils;
import com.sun.org.apache.regexp.internal.RE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author yzw
* @date 2022/1/23 16:25
* @desc redis工具类
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
public boolean set(final Object key, Object value){
if(null == key){
return true;
}
redisTemplate.opsForValue().set(key, value);
return true;
}
public synchronized boolean setnx(final Object key, Object value){
if(null == key){
return true;
}
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
public boolean setExpire(final Object key, Object value, Long seconds){
if(null == key){
return true;
}
redisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
return true;
}
public synchronized boolean setnxExpire(final Object key, Object value, Long seconds){
if(null == key){
return true;
}
return redisTemplate.opsForValue().setIfAbsent(key, value, seconds, TimeUnit.SECONDS);
}
}
<insert id="insertOrder" parameterType="com.blabla.dao.vo.LeaderOrderVO">
insert into leader_order
values (null,now(),now(),#{orderId},#{orderStatus},#{orderAmount},0);
</insert>
运行结果:
更新其实原理一样,主要是setnx的原子操作,考虑好缓存的key 和value 以及过期时间的设置;
|