什么是幂等性
这个概念来源于一个数学公式:
简言之就是:任意多次的执行,对资源本身所产生的影响均与一次执行的影响相同。
什么情况下需要保证幂等性
我们以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); } }