常见写法安全性及效率分析
假设我们的商品表的schema是下面这样的:
1 | CREATE TABLE `goods` (``` |
设置为字段无符号解决
1 | num = select available from goods where id = xx ; |
这种做法大家的想法是我们将库存字段设置成无符号类型,这样当库存字段在sql执行时候被置为负数的时候mysql就会报错,那么affectRow就会是0或者可以捕获到这个异常,从而实现并发下的数据安全。
解法释义
实际上这段代码是危险的,因为在不同版本的mysql和配置下,这段代码的表现完全不同。具体的情况会出现3种不同的结果:
- 代码正常运行,执行update的时候报错
- 代码最终执行结果出现了 -1
- 最终update操作之后,available变成了一个很大的数目
为什么会出现这三种情况呢?
我想在学习开始学习计算机的时候都讲过计算机的加减法计算方法。
思考一下,无符号2 减去 无符号3 在计算机中的运算是什么样的?
2 - 3 = 2 + (-3)
假设我们的计算机是4位的,2的补码表示:0010,-3的补码表示为1011
那么加和的结果是
0010
1011 +
------
1111 =
1111解释为有符号数是多少呢? -1
1111解释为无符号数是多少呢? 15
所以呢?如果mysql不做任何处理的话,你的无符号数减法的结果不会报错,最终你算出来的库存还是一个非常大的值(可怕)。
但是幸运的是mysql 后来的版本帮你做了这件事情(具体哪个版本我也不清楚),所以如果是mysql做了无符号检测的话,如果减出的结果是负值,会报错,这是大多数人期待的结果。-1这种情况是需要你设置一下sqlmode的,这也是会出现的情况。
解法总结
- 这个办法很多人用的时候没问题,那只能说明可能是机缘巧合,但是对于业务代码而言,不能靠碰运气,需要消除不确定性、缩小迁移成本。
- 如果你想采用这种办法,辛苦你把你们msyql相应的版本及配置搞清楚,确定无符号在你所在的版本会出现什么结果。
select for update(感觉并不能保证原子性)
解法释义
读取时候就开始加排他锁也是网上常见的办法之一,具体实现如下:
1 | begin tran ; |
该解法在用户读取的时候对相应的数据加排他锁,保证自己在更新的时候该行的数据不会被别的进程更改.所有写请求及排他锁加锁都会被阻塞。
想想这样的情况,A进程执行过程中,出现死机的情况导致commit/rollback请求没有被发送到mysqlserver,那么所有请求都会锁等待。
解法总结
低流量可以采用这种办法来保证数据的安全性
性能低下,平均需要发送4次mysql请求,同时会造成所有同类请求锁等待。
常见问题
select for udpate 需要在显式的指定在事务代码块执行,不然不会起作用。很多网友都理所当然的人为select for update直接就可以加排他锁
排他锁的释放是在rollback/commit 动作完成才会释放,不是在update操作之后。mysql innodb执行两段锁协议,加锁阶段只加锁,解锁阶段只解锁。
采用事务,先查后写再查,确保没问题
解法释义
这时候的available设置为有符号类型,解决方案一的问题
1 | begin tran ; |
这种解法区分于第一种的办法在于,加了事务、available类型更改、采用了更新后确认的形式,尝试解决问题。
我们都知道数据库的事务隔离级别有4种:
RU,RC,RR,Serializable。
我们常见的innodb中RR模式是可以保证可重复读,意思是在同一个事务内部,多次读取的结果是一致的。那么最后一次的读取对于RR隔离级别实际上是无效的。
RC模式下,这个代码是可用的,每次请求可以确保自己的进程不会超发。
解法总结
- RR、RC模式下结果不一致.RR下不可保证安全、RC可以。
- 性能不高,一次业务请求到mysql的转化为 1 : 5。
- 这种解法就像老奶奶锁门,总是不放心自己到底锁了没有,走了几步再回来看看,实际上有些时候是徒劳。
update语句增加available查询条件
解法释义
1 | udpate goods set available = available - 1 where id = xx` `and` `available - 1 >= 0 ; |
大家有的另一个误区是单条语句不是事务,实际上单条sql也是一个事务。-
问题的关键就集中在怎么证明这句的安全性的。-
我们都知道update操作对于id为主键索引的情况下,是会对数据加行锁。-
其实update操作在mysql内部也是一个先查后改的过程,这个过程如果是原子的,那么可以保证update语句是串行的,那我们就来看一下update语句在mysql内部的执行过程。-
那么对于上面这个语句,一样遵循两段锁协议。-
update执行的过程,会去查询满足条件的行并加锁,这个加锁是innodb做的,那么就可以保证别的事务必须等到该事务执行完了之后才能获得锁,此时拿到最新数据。
解法总结
- 语句安全、效率最优(我的认知里)
采用设置库存而不是扣减库存
这几天我把类似的文章几乎翻了一遍,唯一看到批评我的上一条做法的是我的那个做法是不具备幂等性的。
- 所谓幂等性就是,同一个用户对同一连接的访问不会产生副作用。比如上一条的方案,如果记录用户的操作和扣减库存不是原子操作的话,就有可能出现的问题是,库存扣减成功了,但是用户记录失败了,那么用户重复请求,就会出现多次减库存的问题。
那么他们的解法是这样的,采用设置而不是扣减,代码如下:
1 | num_old = select available from goods where id = xx` `and` `available >= 1 ; |
这段代码也是安全的,采用的是乐观锁的理念来完成的操作。
总结
- 上面的做法,最后两个是相对安全的,但是你的库存字段还是要设置为无符号,关于是否幂等,要看结合请求看,不是单个扣减块代码。
引用
- 何登成的技术博客mysql udpate流程学习
- 幂等性做法来源使用设置库存代替库存扣减
- mysql 无符号问题