Bootstrap

搞懂幂等性

什么是幂等性

这个概念来源于一个数学公式:

在这里插入图片描述

简言之就是:任意多次的执行,对资源本身所产生的影响均与一次执行的影响相同。

什么情况下需要保证幂等性

我们以sql为例,来看看什么情况下有幂等性的问题。这里就以stock(库存表为例)

字段名字段含义
id雪花算法的id值
product_id产品id
product_name产品名称
balance剩余数量

1.查询语句

select * from stock where id =

select语句无论查询多少次都不会对原记录产生影响,天然就具有幂等性。

2. 插入语句

insert into stock(id, product_id, product_name, balance) values (1159819968336375809, 1, '大白兔奶糖', 200)

这里要分情况讨论了。假如id是唯一主键或者唯一索引那么即便重复插入多次,结果也就一条记录入库,此时就具备幂等性;反之,如果id不是唯一主键或者唯一索引那么这条插入语句就不具备幂等性。

3. 删除语句

delete from stock where id = 1159819968336375809

删除一次和删除多次,结果都是一样的。也是天然具有幂等性的。

4.更新语句

update stock set balance = 300 where id = 1159819968336375809

上面的sql无论执行多少次,最后表中的剩余数量都会是300。所以是具有幂等性的。

update stock set balance = balance-1 where id = 1159819968336375809

上面的sql语句每次执行都会造成剩余数量-1,此时就不具备幂等性。

实现幂等性的方案

从上面的分析来看,幂等性可能出现在插入和更新时才有幂等性问题。

1. 保证插入时的幂等性

  • 唯一索引。比如提交一个订单信息,那么用户订单号就可以是唯一索引。这样由数据库来保证幂等性问题。但是分库分表的情况就不太适用了。此时可以由一个中立数据库的表来承担唯一索引校验的工作,校验通过的再执行插入的分库中。

  • token机制防止重复插入。主要采用redis或者redisson来防止重复提交,大概思路就是:在 Redis 中设置一个值表示加了锁,然后释放锁的时候就把这个 Key 删除。

2. 保证更新时的幂等性

  • token机制防止重复更新。

  • 事务隔离级别为SERIALIZABLE【相当于锁表,不推荐】

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void updateStockBalance() {
        ......
    }
    
  • 悲观锁。select for update

    相当于行锁,更新后由spring自动提交事务。

  • 方法上加Synchronized(单体环境并发不是非常大的时候适用)

  • 乐观锁方式。

3. 乐观锁+重试机制

这里介绍下我在项目中经常用到的保证幂等性更新的方式,数据库的操作是用MP来完成的。这里还是拿上面的stock表为例。

  • 表加一个version版本号字段

  • entity对象增加注解

    /**
     * 版本号。用于乐观锁
     */
    @Version
    private Integer version;
    
  • 引入重试依赖

    <!-- spring重试机制 -->
    <dependency>
      <groupId>org.springframework.retry</groupId>
      <artifactId>spring-retry</artifactId>
    </dependency>
    
  • 定义一个版本异常

    /**
     * 版本更新异常,用于版本更新失败时重试
     *
     * @author 老马
     */
    public class VersionUpdateException extends RuntimeException{
    }
    
  • 写更新库存剩余的service(示例代码)

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    @Retryable(value = {VersionUpdateException.class}, maxAttempts = 10)
    public ConfirmResponseBizContent outputAndUpdateRelationInfo(ConfirmContext context) {
      // 查询库存记录
      Stock stock = this.getStockInfo(context);
      if(ObjectUtil.isNull(stock) || stock.getBalance() <= 0) {
        return null;
      }
      context.setStockInfo(stock);
    
      //构建返回信息
      ConfirmResponseBizContent responseBizContent = buildResponseBizContent(context);
      
      // 更新库存剩余。库存剩余-=1
      boolean updateResult =  updateStockBalance(context);
      if(!updateResult) {
        //没有更新成功时,再次执行本方法。尝试执行10次
        throw new VersionUpdateException();
      }
      
      //构建返回信息
      return responseBizContent;
    }
    
    // 更新库存剩余。库存剩余-=1。注意:这里一定要加上版本条件
    private boolean updateStockBalance(ConfirmContext context) {
      return new LambdaUpdateChainWrapper<>(this.stockMapper)
        .setSql("balance = balance-1")
        .eq(Stock::getId, context.getStockInfo.getId())
        .eq(DiscountDaySummary::getVersion, context.getStockInfo().getVersion())
        .update();
    }
    
  • 启动类加入@EnableRetry注解

    @EnableRetry
    @SpringBootApplication
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;