抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

小知识补充

首先,我们要知道在mysql中update操作都是线程安全的,mysql引擎会update的行加上排他锁,其他对该行的update操作需要等到第一个update操作提交成功或者回滚,才能获取这个排他锁,从而对该行进行操作。

例子表结构

upload successful

小知识点:表必备三字段:id, create_time, update_time。
说明:其中id 必为主键,类型为bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型。 (来自《阿里巴巴Java开发手册(华山版)》)

代码环境

  • jdk1.8
  • idea
  • SpringBoot
  • Mybatis Plus

场景演示

现在假设我们要写一个买书的代码(这里为了简单就一次卖一本啦),并使用线程池模拟并发开启30个线程去买20本书,那我们可以十分随意的写出这样的代码。

数据

upload successful

无锁更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public int decrGoodsAmount(long id) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("id",id);
queryWrapper.last("for update");
Goods goods = baseMapper.selectOne(queryWrapper);
if (goods.getAmount()>0){
UpdateWrapper updateWrapper = new UpdateWrapper();
updateWrapper.setSql("amount=amount-1");
updateWrapper.eq("id",id);
return baseMapper.update(null,updateWrapper);
}
return 0;
}
结果

upload successful

结果很明显超卖啦,虽然我们有使用if去判断库存,但是在并发情况下,你观察到的情况可能已经被改变啦

写独占锁更新

我们首先来了解一下 for update语法:

for update是在数据库中上锁用的,可以为数据库中的行上一个排它锁。当一个事务的操作未完成时候,其他事务可以读取但是不能写入或更新。InnoDB默认是行级别的锁,当有明确指定的主键时候,是行级锁。否则是表级别。

小知识:for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public int decrGoodsAmountByLock(long id) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("id",id);
queryWrapper.last("for update");
Goods goods = baseMapper.selectOne(queryWrapper);
if (goods.getAmount()>0){
UpdateWrapper updateWrapper = new UpdateWrapper();
updateWrapper.setSql("amount=amount-1");
updateWrapper.eq("id",id);
return baseMapper.update(null,updateWrapper);
}
return 0;
}
结果

upload successful

CAS更新

除了使用独占锁或者说是悲观锁来控制数据并发安全,还有什么方法呢?我们还可以使用乐观锁CAS来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

@Override
public int decrGoodsAmountByCAS(long id) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("id",id);
int flag = 0,num = 0;
while (flag==0) {
if (++num==3){
return 0;
}
Goods goods = baseMapper.selectOne(queryWrapper);
if (goods.getAmount() > 0) {
UpdateWrapper updateWrapper = new UpdateWrapper();
updateWrapper.setSql("amount=amount-1,update_time=CURRENT_TIMESTAMP()");
updateWrapper.eq("id", id);
updateWrapper.eq("amount",goods.getAmount());
flag = baseMapper.update(null, updateWrapper);
}else{
return 0;
}
}
return 1;
}
结果

upload successful

upload successful

我们可以看到在尝试3次后,任然有22个线程尝试失败,这是因为并发太过激烈的原因。那么什么时候使用CAS操作呢?

阿里巴巴的开发规范上提到,在并发不高的情况下(尝试失败率不超过20%的情况下),推荐用CAS更新。

总结

  • 使用独占锁来解决并发问题是不错,但我觉得不太常用。
  • 使用CAS来解决并发问题也不错,甚至不用加事务,而且不会堵塞读取操作。
  1. 如果对读的响应度要求非常高,比如证券交易系统,那么适合用乐观锁,因为悲观锁会阻塞读
  2. 如果读远多于写,那么也适合用乐观锁,因为用悲观锁会导致大量读被少量的写阻塞
  3. 如果写操作频繁并且冲突比例很高,那么适合用悲观写独占锁

学习资料

简书

思否