Spring事务管理下加锁,为啥还线程不安全?

前言

在单体架构的秒杀活动中,为了减轻DB层的压力,这里我们采用了Lock锁来实现秒杀用户排队抢购。然而很不幸的是尽管使用了锁,但是测试过程中仍然会超卖,执行了N多次发现依然有问题。

代码演示

先上代码:

@RestController
@Slf4j
public class SeckillDistributedController {
    @Resource
    private ISeckillDistributedService seckillDistributedService;

    @PostMapping("/startSeckilLock")
    public Result startSeckilLock(long seckillId) {
        final long killId = seckillId;
        log.info("开始秒杀");
        for (int i = 0; i < 10000; i++) {
            final long userId = i;
            Runnable task = () -> { {
                    Result result = seckillDistributedService.startSeckilLock(killId, userId);
                }
            };
            executor.execute(task);
        }
        return Result.ok();
    }
}

@Service
public class SeckillServiceImpl implements ISeckillService {
    private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁
    @Resource
    private DynamicQuery dynamicQuery;

    @Override
    @Transactional
    public Result startSeckilLock(long seckillId, long userId) {
         try {
            lock.lock();
            String nativeSql = "SELECT number FROM seckill WHERE seckill_id = ?";
            Object object =  dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
            Long number =  ((Number) object).longValue();
            if(number > 0){
                nativeSql = "UPDATE seckill SET number = number - 1 WHERE seckill_id = ?";
                dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
                SuccessKilled killed = new SuccessKilled();
                killed.setSeckillId(seckillId);
                killed.setUserId(userId);
                killed.setState(Short.parseShort(number + ""));
                killed.setCreateTime(new Timestamp(new Date().getTime()));
                dynamicQuery.save(killed);
            }else{
                return Result.error();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        return Result.ok();
    }
}

出现问题

多线程运行同一个事务管理下加锁的方法,方法内操作的是数据库,按正常逻辑得到最终的值应该是100,但经过多次测试,最终结果出现超卖101,如下图所示:

这是为什么呢?

问题分析

追踪

事物未提交之前,锁已经释放(事物提交是在整个方法执行完),导致另一个事物读取到了这个事物未提交的数据,出现了脏读。

数据库层面分析

数据库默认的事务隔离级别为可重复读(repeatable-read)[^脚注1],也就不可能出现脏读[^脚注2],但会出现幻读[^脚注3]。查询数据库可知,如下图所示:

代码层面分析

代码写在service层,bean默认是单例的,也就是说lock肯定是一个对象。

总结

这里,总结一下为什么会超卖101:秒杀开始后,某个事物在未提交之前,锁已经释放(事物提交是在整个方法执行完),导致下一个事物读取到了上个事物未提交的数据,出现传说中的脏读。

解决方案

此处给出的建议是:锁上移,上移到Controller层,包住整个事物单元。修改代码为:

public Result startSeckilLock(long seckillId) {
        final long killId = seckillId;
        log.info("开始秒杀");
        for (int i = 0; i < 10000; i++) {
            final long userId = i;
            Runnable task = () -> { {
                try{
                    lock.lock();
                    Result result = seckillService.startSeckilLock(killId, userId);
                }finally {
                    lock.unlock();
                }
                }
            };
            executor.execute(task);
        }
        return Result.ok();
    }

修改完成后,重新测试一下代码。意料之中,再也没有出现超卖的现象。

源码解析

@Transactional 切片是一种特殊情况

1)多 AOP 之间的执行顺序在未指定时是 :undefined ,官方文档并没有说一定会按照注解的顺序进行执行,只会按照@ Order的顺序执行。

参考官方文档: 可在页面里搜索 Control+F/Command+F「7.2.4.7 Advice ordering」

2)事务切面的 default Order 被设置为了 Ordered.LOWEST_PRECEDENCE,所以默认情况下是属于最内层的环切。

解析源码可知:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
    int value() default 2147483647;
}

public interface Ordered {
    int HIGHEST_PRECEDENCE = -2147483648;
    int LOWEST_PRECEDENCE = 2147483647;

    int getOrder();
}

参考官方文档: 可在页面里搜索 Control+F/Command+F「Table 10.2. tx:annotation-driven/ settings」

可重复读: 每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据。

脏读: 一个事务读取到另外一个事务未提交的数据

幻读: 是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。

转载于:https://my.oschina.net/loubobooo/blog/3016020

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值