1、事务的相关概念和集成过程就不在这里重复,可看本人另一篇https://blog.csdn.net/qq_20475615/article/details/93713519
2、这次主要是探索并发中数据的问题,场景是电商系统下单减库存,mysql,暂没涉及分布式和集群
3、所有测试我们先预设原商品库存为100,且我们通过用户不同来指定休眠更好的看效果,admin为休眠的用户它下单1个商品,另一个请求下单3个商品
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_UNCOMMITTED)
@Override
public boolean checkAndLockStock(Map goodsMap, String orderId) {
if(null == goodsMap){
throw BusinessException.of(BusinessMessageEnum.E_ERROR);
}
List logs = Lists.newArrayList();
for(Map.Entry entry:goodsMap.entrySet()){
MallGoods goods = this.getById(entry.getKey());
if(ObjectUtils.isEmpty(entry.getValue()) || ObjectUtils.isEmpty(goods.getStock()) || goods.getStock() < entry.getValue()){
throw new BusinessException(BusinessMessageEnum.E_AMOUNT_NOT_ENOUGH.getMsg());
}
boolean result = this.baseMapper.updateGoods(goods.getId(),entry.getValue());
if("admin".equals(securityUtil.getCurrUser().getUsername())){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// throw BusinessException.of(BusinessMessageEnum.E_ERROR);
}
if(!result){
throw BusinessException.of(BusinessMessageEnum.E_ERROR);
}
}
return true;
}
首先是admin请求先发,然后更新后不提交事务,进入休眠,接着另一个用户请求过来发现读到的数据已经是admin没提交的事务的数据,如果在休眠后让admin那个线程回滚,脏读就出现了
把隔离级别改一下就可以了
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_COMMITTED)
隔离级别为 READ_COMMITTED 脏读没了,但是在admin请求过来后更新了数据但不提交事务,另一个请求过来一阵操作并更新了数据提交事务,然后admin休眠到了才提交事务,结果另一个请求东西没了,本来结果应该是去掉 3+1,为96,但数据库结果是99,也就是前面先提交事务的减3不见了
改级别
@Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ)
REPEATABLE_READ 是在写的时候给该条数据加锁,前面更新的事务没提交后面的事务是提交不了因为数据被锁住了。 这个级别保证的就是修改数据的提交顺序
实际上比如减少库存在这个REPEATABLE_READ 隔离级别下还出现数据错乱是因为在业务先查询之后计算,再保存计算的值有问题,它保证的是你写完的数据不丢失,而不是保证你读完到写完,比如我一开始用的
Integer before = goods.getStock();
Integer after = before - entry.getValue();
goods.setStock(after);
boolean result = this.updateById(goods);
而更新的顺序其实是没问题的,假如自己写sql用 update t_mall_goods set stock = stock - #{value} where id = #{id} ,保证顺序性之后拿最新的值而不是预先计算好,则没问题。
那假如我们没有这个顺序性呢,就导致A更新后还没提交,B更新后没有限制直接提交,那A刚才update拿的即使是最新值也没用,那个最新值在B更新之前的,结果就是B的更新没有了
上面说到 REPEATABLE_READ不保证读完到写完,SERIALIZABLE 便可以,最严格的老师,只要它读过,别人就只能读不能写,就相当于我看你一眼你就是我老婆,剩下的别人只能看不能动了,得等到我放你自由
用 REPEATABLE_READ 解决,是加写锁,这在商品里会带来的问题,下订单整个事务可能涉及多个商品,而如果两个请求各锁住一个商品,各自等对方的事务提交,就死锁了。
@Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ)
@Override
public boolean checkAndLockStock() {
Map goodsMap = Maps.newLinkedHashMap();
if("admin".equals(securityUtil.getCurrUser().getUsername())){
goodsMap.put("194528707925250048",1);
goodsMap.put("152869740279238656",1);
}
if("general".equals(securityUtil.getCurrUser().getUsername())){
goodsMap.put("152869740279238656",2);
goodsMap.put("194528707925250048",2);
}
for(Map.Entry entry:goodsMap.entrySet()){
MallGoods goods = this.getById(entry.getKey());
if(ObjectUtils.isEmpty(entry.getValue()) || ObjectUtils.isEmpty(goods.getStock()) || goods.getStock() < entry.getValue()){
throw new BusinessException(BusinessMessageEnum.E_AMOUNT_NOT_ENOUGH.getMsg());
}
boolean result = this.baseMapper.updateGoods(goods.getId(),entry.getValue());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!result){
throw BusinessException.of(BusinessMessageEnum.E_ERROR);
}
}
return true;
}
上面故意让两个订单下两个相同商品,但是相互执行顺序调换,结果
实际上这种问题之所以出现因为我是单个商品更新的,如果商品是批量更新,尽管底层还是一条条执行,但这个时间差很小,没有在业务层一条条更新这么大的时间差,就基本不会有这种在业务层出现死锁的问题。
其实我这里是为了看效果用了LinkedHashMap按照我们存入的顺序,实际上用HashMap,存储不按我们的顺序再hash之后,存储重叠的商品读取出来前后顺序都是一致的。但如果用的其它结构比如直接商品实体的list之类的,就需要注意这个死锁问题。
如果希望自己读完到写完的过程中在业务层进行计算又能最后数据保持一致,可以使用下面的几种方式
1、代码锁:这种就是简单粗暴,代码上加 synchronized 锁
2、悲观锁:这种就是用前先锁住,其他人等着
3、乐观锁:这种就是大家都能改,最后看谁快,慢的就回滚或者再获取新值进行操作
4、记录锁:这种就是类似悲观锁,专门用一个表来作为锁,所有线程去创建一个记录,创建成功的也就是拿到锁,当然这个需要考虑拿锁的线程如果崩了没有及时删除记录导致其它线程都等待到超时了,所以要加个定时任务去处理这个异常记录。