讨论 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

============================
4.18更新

有一种情况,会出现不“原子”的情况:

调用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的


附:


posted on 2018-02-08 23:37  silyvin  阅读(1111)  评论(0编辑  收藏  举报