mysql事务锁等待是公平锁吗_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,如下图所示:

4de92f4532a80d9fefe98cdb8f465930.png

2ff4421f5a8754212305b84bb2e17273.png

这是为什么呢?

问题分析

追踪

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

数据库层面分析

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

e485db2dc12262b8c81973d199ca35b6.png

代码层面分析

代码写在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」

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值