整理一下事务处理相关内容。

一个典型场景就是我们需要删掉一个用户,跟这个用户相关其他的东西也应当一并删除。 这堆东西可能会跨几张表,就要好几个语句。 但是因为这个用户可能在两个客户端上同时操作同一个账号,就会产生并发问题。 如果有的语句成功,有的失败,也就是删不干净,那这个用户因为黑历史没有抹杀掉,但他号又没了根本登不上,他就会非常难受。 所以这个时候,就要用到事务处理。

事务处理的目的就是简化并发情况下访问数据库避免爆炸的问题。

ACID

一般情况下,一个事务应当具有这四个特性:原子性、一致性、隔离性、持久性。

原子性就是说一个事务内的语句要么全部成功,要么全部失败。 比如说在一个事务里删一个用户的所有相关信息,成功的话就全部删干净,失败的话就保持原样,避免用户登录不上去了结果黑历史还留着删不掉的尴尬情况。

一致性就是指事务之前与事务之后数据库应当保持一致性。 可以理解成这些数据在经历了一个事务之后,仍然遵守某种规则。 最简单的就是要遵守表的结构,当然一条记录必定会满足这个条件。 更多的情况是,这些数据应当遵守程序员在业务逻辑上规定的条件。 比方说两个用户之间赠送物品,总归是一个用户甲失去物品,另一个用户乙得到物品。 那么规则就是这个物品只能有一个,不能出现甲失去物品时宕机结果这个物品就丢了,或者乙得到物品时宕机然后这个物品就甲和乙都持有这类的情况。 所以保证一致性实际上是需要数据库跟程序员共同完成的,程序员需要合理设计事务才能保持数据的一致性。

隔离性就很简单了,就是事务与事务之间是相对独立的,可以减少事务之间的互相干扰。 但是隔离是有等级的,不同的等级隔离的效果也不一样,后面会提到。

持久性,事务处理成功结束后应当将对数据的修改持久化,此后就不会受到宕机的影响。

因为并发读写操作是可能产生冲突的,因此我们需要用锁机制来避免并发的时候爆炸。 锁机制的实现是非常复杂的。 如果要谈到排他锁、共享锁、更新锁、意向锁这些锁的类型,必须要结合数据库的具体实现来谈,不一定具有普适性,而且也不是两三句话能讲清楚的。 这里为了简单,我们考虑从锁的粒度来进行分类。

  • 行锁。

    就是对一条记录加锁,比如说修改一条记录时就要加行锁。

  • 间隙锁。

    对一段区间内的数据加锁,比如说更新一个范围内的所有数据时就需要间隙锁。 注意,间隙锁需要索引的支持,如果这个范围的字段没有建好合适的索引,那它就会升级为表锁。 而且也不是所有的数据库都支持间隙锁,这个也要看具体实现。

  • 表锁。

    直接锁住整张表,实现会比较简单,上锁的开销也比较小,但是显然并发能力很弱。

隔离级别

有四种级别:未提交读、已提交读、可重复读、串行。 首先假设我们有两个数据库连接,然后每个连接各跑一个事务,分别为事务甲和事务乙。 然后在各个隔离级别下分别介绍它们的隔离效果。

  • 未提交读。

    事务甲与乙都没有提交,它们的修改双方都能读到。

  • 已提交读。

    事务甲与乙只能读到提交后的修改。 但是如果甲先读了一条记录,乙修改这条记录提交后,甲再读这个记录就会发现它被改变了,这就是不可重复读问题。

  • 可重复读。

    一个事务对于提交后的修改都无法感知,除了新记录的插入。 一个事务能够感知到新纪录的插入,这就是幻读问题。

  • 串行。

    就是甲和乙只能一个一个排队执行。 不会有并发的问题,因为是串行的。 我怎么感觉是废话。

并发问题

其实隔离级别里面已经说的差不多了,但是还是来总结一下。 还是假设我们有两个数据库连接,每个连接各跑一个事务,分别为事务甲和事务乙。

  • 脏读。

    未提交之前,甲和乙之间都能够互相读到对方的修改。

  • 不可重复读。

    乙读一条记录,甲修改完这条记录之后提交了,乙再读结果发现跟之前读的不一致。

  • 幻读。

    乙查询总记录数,甲插入了一条新纪录后提交了,乙再读就发现记录数量多了一条。

实现浅析

数据库中应用最广泛的就是平衡树,事务处理要解决的问题实际上很大程度上都是并发访问平衡树的问题。

TODO: 有空填坑。