1、问题介绍
今天在工作上遇到一个问题:
数据库表 TABLE 中有一个字段 NUM,现要将它从数据库中取出来,经过一番运算后,再将其存入 TABLE 表中。
我们的问题就在于,在并发情况下,我们取出来的 NUM 也许会在我们进行运算的时候被其他人修改掉,而当我们将运算后的数据存入表中时,就会覆盖其他人的操作。
2、问题琢磨
当运算只是简单的加减运算的时候,我们考虑利用sql语句的特性,直接对其修改,如下:
update TABLE
set NUM = NUM + 1
where ID = #{id}
当MyBatis执行此sql语句时,会自动对 NUM 字段进行加锁,防止其他用户的修改。
但是这种解决方法只适用于能在一条 sql 语句中完成运算的情况,当运算过程中夹杂其他数据库操作时,就得借用其他字段来完成事务了。
3、解决方案
举例如下:
- 在 TABLE 表中新增一个 VERSION 字段
- 从表中取出 NUM 和 VERSION 字段,NUM 值为 1,VERSION 值为 1
- 对 NUM 值进行运算,其中夹杂其他数据库操作,此时 NUM 值变为了 2
- 使用如下 SQL 语句对表数据进行更新:
update TABLE
set NUM = #{num}, VERSION = VERSION + 1
where
ID = #{id}
and VERSION = #{version}
4、方案原理
根据上述 SQL 语句可看出当 NUM 字段进行修改的时候 VERSION字段也会同时进行修改。
现假设两种情况
- 在运算过程中没有其他人对NUM进行修改,那么表中的 VERSION 值并未被修改,该 SQL 语句可以从表中找到应该被修改的数据行,从而对 NUM 和 VERSION 进行修改。
- 在运算过程中有其他人对NUM进行了修改,那么表中的 VERSION 值就会被修改,比如修改为了 2,那么上述 SQL 语句在运行的时候,找的还是 VERSION 为 1 的数据行,但此时 VERSION 值已经变成 2 了,从而就会找不到该数据行,以此避免其他人对 NUM 字段的修改被当前 SQL 语句覆盖掉。
5、注意事项
一般来说我们写代码的时候,每个方法都是具备一个事务,当方法内部有异常发生的时候,就会进行事务回滚。而当我们使用上述解决方案时,该 SQL 语句只是找不到需要被修改的数据行,而不是抛出异常,因此当我们执行该语句时,要对其返回参数进行判断,如果返回参数为 0,也就是 SQL 语句更新影响的行数为 0,即表示在运算过程中有其他人对 NUM 值进行了修改,那么我们要手动抛出一个异常,以此触发事务回滚,以便用户重新操作。