文章

mysql事务相关

事务

如果一次性提交很多条相关的语句。例如删除玩家,并删除相关的数据。这些数据往往需要好多个sql语句才能完全清空。这就导致了如果访问的是一些临界区的例子(打个比方),可能会出现一些一致性的问题。理论上我们可以在驱动数据库的程序内对临界区代码加锁,但是太麻烦了。sql数据库一般会有事务功能,他相当于一批sql数据的打包,不仅保证原子性(这个和汇编的原子性不一样),要么全执行要么全不实行。同时保证一些一致性问题,不会因为多线程的原因被其它线程的请求破坏了数据。

一般事务被认为具有ACID的特性。下面会讲如何实现这些特性的。

事务的控制

一般情况下,sql会默认启动自动提交功能,他会将单条sql语句当成一个事务自动执行。也可以关闭自动提交,将希望一起执行的语句用BEGIN和COMMIT包裹。也有相当于break或者return的代码,ROLLBACK(回滚)。也有隐式事务提交,这个一般是sql浏览器做的,在隐式事务中,无需使用BEGIN TRANASACTION 来开启事务,每个SQL语句第一次执行就会开启一个事务,直到用COMMIT [TRANSACTION]来提交或者ROLLBACK [TRANSACTION]来回滚结束事务。

事务的ACID实现

不同的事务(可以看成微程序)同时运行时自然也会出现一些问题。因为他们都是对于临界区的某一种访问、修改,因此多线程中可能出现的问题都有可能出现。主要有修改丢失、脏读、虚读和幻读四种。

  • 修改丢失非常简单,两个事物同时进行对同一个行写,写的东西相互覆盖了,想成两个a+=1并行执行就行。这个加一个写锁就行。

  • 脏读则是读写同时进行。然后A事务写的时候B读了,然后A失败回滚了,相当于B读了数据库里面不存在的数据。这个不同于另外一种存在于我的某种记忆里面的脏读不一样,读到部分新部分旧的数据不属于脏读。这个是没开启读写锁,但是可以读到未提交、但是已经更改的数据导致的。

  • 不可重复读则是在只允许读取已经提交的数据情况下,且开启读写锁导致的。即在读事务的中间,在一个select读完数据释放锁后,另一个事务的写命令修改了这一行,之后释放互斥锁,之后第一个事务的另一个读命令读取了这一行,这样就出现了一个事务的读取不一致的问题。这个要用一个非常复杂的方法才能做到不拖累效率,同时不大面积加锁的情况下能做到。

  • 幻读。当A事务进行一个select选出了一些数据,即使做到可重复读,但是B事务在A的select的where范围内插入了一行,那么A再一次读取这个select时,就会多读入一行。这个可以使用快照读、当前读(间隙锁或者next-key锁)或者放弃并行,完全串行的执行事务方法来解决。

这里好像有点问题,当前读、快速读好像是区分rr(可重复读)的实现的,幻读还是要靠S(序列化)。快照读是一种变形的行锁。

因此sql标准区分出了四种不同的事务隔离模式:

  • RU(未提交读)
  • RC(提交读)除mysql以外的基本上数据库都是这个
  • RR(可重复读)
  • 序列化

sql提出了这四种程度,但是怎么实现就是具体发行版本的公司做的。

注意事务之间的互斥与行之间读写的互斥的区别。

注意共享读锁和排他写锁的区别。

当前读、快照读

并发控制一般分为两种pcc(悲观并发控制)、occ(乐观并发控制)。pcc的本质精神就是上锁。一般写多读少的时候效率会比较高。occ的思想就是不加锁。一般用不停循环CAS实现(比较交换)。但是在cas是在软件层的,虽然没有加锁进入核心态切换的成本,但是如果写多读少的话,就会在循环、比较中反而浪费更多的成本。

occ在数据库里面的实现也是依靠于以上的流程,不过是对于字段加锁的。

不过即使pcc、occ成本再小,实际上也是延迟了访问。虽然这种行为可以让select读取到最新的数据,但是有些时候可能并不是非常重要,特别是可重复读的情况下,两次都要读到一样的最新数据成本很高。

因此引入MVCC(多版本并发控制)机制,这个可以想象成git。MVCC的实现,是通过保存数据在某个时间点的快照来实现的。每个事务读到的数据项都是一个历史快照,被称为快照读,不同于当前读的是快照读读到的数据可能不是最新的,但是快照隔离能使得在整个事务看到的数据都是它启动时的数据状态。而写操作不覆盖已有数据项,而是创建一个新的版本,直至所在事务提交时才变为可见。

当前读指的是读取的永远是当前最新数据,快照读读的是快照版本的。MVCC的好处就是可以不用加锁、坏处就是额外的存储成本与快照读

可以看一下,mvcc的具体实现、在rc、rr的情况的不同与mvcc对锁的替代方式。

License:  CC BY 4.0