更新丢失、写偏、幻读:数据库事务从快照隔离到可序列化(一,二,三,番外)
我发现,很多时候,人类学习(其实机器学习更是如此)新概念的难度来自于作为参考的案例不多。
为了讲清楚快照隔离(snapshot ioslation)相对于可序列化(serializable)可能产生的问题,我想多举几个例子,再针对它们的相同和不同之处进行讨论。所以这一篇全都是例子,等下一篇再分析。
例1
出自卡内基梅隆大学高级数据库系统(15-721 Advanced Database Systems)课程对写偏(write skew)的解释。
有四个棋子分别为黑、黑、白、白。
事务甲:把所有白色棋子变成黑色。
事务乙:把所有黑色棋子变成白色。
每个事务要做的事情都是:第一步,查找所有白(黑)色棋子;第二步,把找到的棋子改成黑(白)色。
如果是可序列化隔离级别,可以假装两个事务先后发生,最后结果要不是全黑,要不是全白。
但在快照隔离下,如果两个事务都是在对方做第二步之前就做了自己的第一步,事务甲会把那两个原先黑的改成白的,事务乙把那两个原先白的改成黑的,最后变成了白、白、黑、黑。这是在两个事务先后时不可能出现的情况。
例2
出自《数据密集型应用系统设计》。
值班系统记录了每个医生分别是否在值班。为了保证至少有一位医生在值班,当一个医生要下班时,会运行这样一个事务:
查找有多少医生正在值班,如果数量大于1,可以把这个医生改为下班,否则不行。
假如现在有两个医生正在值班,她们近乎同时想要下班。
快照隔离下,两个医生分别使用一个事务,两个事务有可能同时查找有多少医生正在值班,等它们都得到了结果(结果都是2)之后,才分别改为下班。这样,值班的医生就一个都没有了。
而在可序列化下两个事务先后进行就不会出现这样的问题。
例3
也出自《数据密集型应用系统设计》。
一个网站的用户管理系统,规定每个用户都要用独一无二的用户名。
假设这个表的主键(primary key)就是用户名。当有一个用户要创建账号时,会有个事务先查找是否有这个用户名的记录,如果没有,就可以创建。
跟例2一样,在快照隔离下,可能两个同时发生的事务想创建相同的用户名,它们查找时都发现这个账号不存在,便重复为这个用户名创建了账号。后果可能是一个账号的信息覆盖了另一个。
例4
我在例3的基础上稍加创作。
因为疫情,霍格沃茨今年将入学的方式改成了使用门钥匙。但为了有足够的时间做核酸检测和隔离,要求每个学生入学时间至少相差60秒。学生可以在魔法部门钥匙办公室的系统申请入学时间,时间可以精确到比纳秒还小。
为了实现这个需求,魔法部使用了一个有序的(而不是基于哈希的)数据库,在其中用入学时间作为主键。
每当有学生申请一个入学时间,就有个事务会范围搜索(range query)在它前后各60秒的范围内是否有其它人入学,如果没有才能成功创建。
某对调皮的双胞胎不想分开入学,就几乎同时申请了几乎同时的入学时间。在快照隔离下,两个事务有可能会都没看到对方插入的记录,从而让两条记录都创建成功。
例子终于讲完了,下面请移步至第三篇看我对这四个例子的分析。