并发数据问题之幂等设计

3 篇文章 0 订阅
2 篇文章 0 订阅

并发数据问题

技术是解决问题慢慢出现的,不是凭空设计的。

幂等

定义:

接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的 ;

幂等的实现:

  • 数据库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代码加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
  • ignore 和 on duplicate key 的差别你知道吗?
    • ignore会全部更新,丢失数据
    • on duplicate key 存在才更新

更新

数据库乐观锁实现,主要是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 以及过期时间的设置;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值