讨论 update A set number=number+ ? where id=?的原子性 (含数据库原理)
结论:
1. 本句本身具有原子性
2. 当前读(包含update等写入操作)锁定数据,直到事务提交
https://www.zhihu.com/question/46733729/answer/128582074
阿里的mysql大咖给出了肯定的答案
比如,表名A,字段名为 number,如下的SQL语句: 语句1:update A set number=number+ 5 where id=1; 语句2:update A set number=number+ 7 where id=1; 假设这两条SQL语句同时被mysql执行,id=1的记录中number字段的原始值为 10,那么是否有可能出现这种情况: 语句1和2因为同时执行,他们得到的number的值都是10,都是在10的基础上分别加5和7,导致最终number被更新为15或17,而不是22?
首先,他们同时被MySQL执行,你的意思其实就是他们是并发执行的,而并发执行的事务在关系型数据库中是有专门的理论支持的-ACID,事务并行等理论,所有关系型数据库实现,包括Oracle,MySQL都需要遵循这个原理。
简单一点理解就是锁的原理。这个时候第一个update会持有id=1这行记录的排它锁,第二个update需要持有这个记录的排它锁的才能对他进行修改,正常的话,第二个update会阻塞,直到第一个update提交成功,他才会获得这个锁,从而对数据进行修改。
也就是说,按照关系型数据库的理论,这两个update都成功的话,id=1的number一定会被修改成22。如果不是22,那就是数据库实现的一个严重的bug。
如果想要深入了解,可能你要预读一下数据库与事务处理 (豆瓣)了。
粗浅一点的,可以先看一下如果有人问你数据库的原理,叫他看这篇文章和英文原文How does a relational database work
============================
有一种情况,会出现不“原子”的情况:
调用sql的函数加上事务,且rc以上的隔离级别
线程A 线程B
开启事务
开启事务
x=x+1
x=x+1
commit
commit
如果是事务情况下,B在A未提交事务的情况下,是读不到A线程x+1的结果的,那么实际还是累加的1,错误:
以下的代码就会出错 (例子1)标准的防脏读
@Transactional(isolation = Isolation.READ_COMMITTED) public List<Map<String,Object>> listForDirtyReadOneFunction() { List<Map<String,Object>> map = jdbcTemplate.queryForList("select * from tao where col1 = 0"); // try { // Thread.sleep(10000); // } catch (InterruptedException e) { // e.printStackTrace(); // } jdbcTemplate.update("update tao set col2 = 'xxxx' where col1 = 0"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } return map; }
真的是这样么,又反转了
仔细来看这个例子:
这个例子是一个典型防脏读的例子,读取是快照读,而我们本文讨论的update是当前读,不可同日而语
例子2:查看当前读(含update)与rc隔离级别的关系,实践证明当前读在事务中会阻塞其它当前读,直到本事务提交
@Transactional(isolation = Isolation.READ_COMMITTED) public List<Map<String,Object>> listForDirtyReadOneFunction() { jdbcTemplate.update("update tao set col3 = col3+1 where col1 = 0"); List<Map<String,Object>> map = jdbcTemplate.queryForList("select * from tao where col1 = 0"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } return map; }
这个例子,无论如何col3都被累加了2次,故
这个模型是错误的,实践下来是这样的:
开启事务
开启事务
x=x+1 阻塞
sleep 10s
commit
x=x+1
sleep 10s
commit
其中当中两句update,都是当前读,都使用lbcc先取得锁,继而读到最新的数据
而快照读则符合我们对脏读一般的理解,使用mvcc读历史数据
脏读定义:对于其它事务已执行未提交的数据,本事务的快照读读取到了,注意是快照读;
在rc以上隔离级别下,快照读使用mvcc读取历史数据,而update是当前读,读取时会读到最新数据,会给数据加锁后修改
看下例子:
@Transactional public void insertForDirtyReadAndIllusion () { jdbcTemplate.execute("insert into tao values (1,'d',0)"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } // int a = 1/0; }
// a先执行insert 睡觉 b执行此函数,查看是否能更改到a未提交但已插入的数据 // 实践证明:读到且改到,随着a的睡眠而阻塞 @Transactional(isolation = Isolation.READ_COMMITTED) public List<Map<String,Object>> updateReadForDirtyRead() { jdbcTemplate.update("update tao set col2 = 'update' where col1 = 1"); return null; }
这里得到一个很重要的结论 当前读(包含update等写入操作)锁定数据,直到事务提交
再来看这个模型:
根本不存在的,x=x+1未commit前,其它线程除了快照读,是读写不到x的
附: