事务
事务的棘手性
ACID
单对象和多对象操作
-
单对象写入
-
对单节点上的单个对象(例如键值对)上提供原子性和隔离性
- 原子性通过日志做崩溃恢复
- 隔离性通过加锁实现
-
-
多对象事务
- 关系型数据库更新带有外键的表
- 文档型数据同时更新多份文档
- 二级索引与数据的同时更新
-
处理错误和中止
- 对于已经提交的事务,如果在给客户端返回结果的网络中断,如果此时重试事务,则需要去重机制
- 如果是由网络拥塞导致的重试,则需要限制重试次数
- 非必要问题不用重试,如违反约束不需要重试
- 两阶段提交
弱隔离级别
相比可序列化的强隔离性之外都算做弱隔离级别
读已提交
-
脏写
- 同时写入后面的写入覆盖前一个
- 解决方法:加行锁
-
脏读
-
读到未提交的数据
-
解决方法
-
与解决脏写的方法一样,加行锁,但如果写事务数据时间过长,读请求会有较大的时间延迟
-
保存数据的旧值,在事务未提交时读到旧值,事务提交后读到新值
- 如果读取事务的时间范围内出现了写事务的提交,则读事务中相同的两次或多次读取事件所读到的数据可能会不同
-
-
快照隔离和可重复读
-
支持数据库:PostgreSQL oracle MySQL(innodb) SQL Server
-
实现快照隔离
-
对于PostgreSQL,按如下方法实现MVCC
- 基于txid事务ID实现,每个事务开始时均有一个唯一的TXID,约40亿次后溢出
- 表中每一行数据都有created_by与deleted_by字段,分别存储写入时的txid
- 删除时标记deleted_by,不实际删除,在稍候的时间,当确定没有事务再访问已标记删除的行时,垃圾回收器将该行移除,释放空间
- 对于UPDATE操作实际执行了两步操作:DELETE INSERT
-
拓展(非本书内容):MYSQL5.6 MVCC实现
-
仅存在于读已提交与可重复读两种隔离级别,特别地,对于可重复读,使用了MVCC+行锁实现,读时不加锁,写时加锁
-
在MYSQL中如果事务更改了某行,则会针对该行生成UNDOLOG(记录了该行之前的记录内容),在发生回滚时,将UNDOLOG该行的记录恢复
-
行锁 间隙锁
- MYSQL:基于事务ID,比如undolog中存了v7这条回滚数据,而现在最小的活跃事务ID是v8,那v7这条数据就不可能再被读到,就可以删除了。
-
-
-
-
观察一致性快照的可见性规则
- 列出所有尚且未提交或尚未终止的事务列表,即使这些事务后来提交了,忽略这些写入
- 具有较晚事务ID的都将被忽略
-
索引和快照隔离
- PostgreSQL:同一对象的不同版本可以放入同一个页面中,PostgreSQL的优化可以避免更新索引
- CouchDB,Datomic和LMDB:为修改页面创建一个索引副本,从父级到根进行级联指向更新指向新版本
-
可重复读与命名混淆
防止丢失更新
-
定义
- 并发写入冲突(丢失)问题(并发的数据库读值–更改数据–写回数据操作)
-
解决方法
-
原子写
-
如UPDATE XX SET a=a+1
-
实现方案
- 在对象上加排它锁
- 强制原子操作在单一的线程上执行
-
-
-
显式锁定
-
在查询范围上加排它锁
-
-
自动检测丢失更新
- PostgreSQL的可重复读,Oracle的可串行化和SQL Server的快照隔离级别,都会自动检测到丢失更新,并中止惹麻烦的事务。但是,MySQL/InnoDB的可重复读并不会检测丢失更新
-
Compare and Set
-
冲突解决和复制
- 允许写入多个冲突值,并在应用层进行合并
-
写入偏差与幻读
-
场景
- 一个SELECT查询找出符合条件的行,并检查是否符合一些要求。
- 按照第一个查询的结果,应用代码决定是否继续。
- 如果应用决定继续操作,就执行写入(插入、更新或删除),并提交事务。
- 如两个人同时抢购1件商品,二人都先查到还要一件库存,在应用程序当中,二者均向数据库发出减库存的写入请求,此时会出现库存异常
-
快照隔离解决了读事务中出现的幻读情况,但会出现写入偏差
-
-
可序列化
背景:
- 很难检查代码在特定隔离级别下是否安全
- 并发测试困难
真的串行执行
-
单线程执行并发事务
-
产生背景
- RAM足够便宜,可将事务需要的活跃的数据集放在RAM当中,而不是之前放在磁盘上,执行效率提高了
- OLTP事务通常执行速度很快,只进行少量的读写操作
-
实例:VoltDB/H-Store,Redis和Datomic
-
在存储过程中封装事务
-
存储过程的优缺点
-
分区
- 对于可以把数据划分为多个分区,并且每个事务只在一个分区上进行,即可实现事务随分区数量增加而线性扩展。对于跨分区的事务,则不能通过增加机器提升性能
二阶段锁定
-
相比较快照隔离,二阶段锁定基于读不阻塞写,写阻塞读的方案,解决了写入偏差与丢失更新
-
实现2PL
-
分为共享锁与排它锁
- 多个事务可同时持有共享锁
- 有一个写入事件则升级共享锁为排它锁,该锁阻塞其它读取与写入,直到事务完成
- 性能相比较弱隔离级别差得多
-
-
谓词锁
-
为了防止范围查询出现幻读的情况,需要一种范围锁(非指定对象,比如表中某一行的锁),需要实现以下两点
- 排它阻塞读–任一读取事务必须等到持有排它锁的事务结束后才可进行读取操作
- 任意一种锁(排它与共享)阻塞排它锁
-
-
索引范围锁(间隙锁)
-
谓词锁由于性能不佳(锁粒度过小,检查锁匹配非常耗时),多数数据库实现了索引范围锁
- 如房间预订请求,查询条件为某时间段与房间号,如果表分别在时间段和房间号建立了索引,则索引范围锁会将其中一个索引(时间段或房间号)上附加共享锁,如果其它事务的写入操作需要修改该索引,则它会等到上述共享锁释放
-
可序列化快照隔离
-
在快照隔离的基础上,SSI添加了一种算法来检测写入之间的序列化冲突,并确定要中止哪些事务。(乐观锁)
- 如果出现了大并发写入事务争抢同一写入对象,会导致一大部分写入事务需要终止,如果系统已经接近最大吞吐,则重试事务可能会导致性能变差
-
快照隔离会出现写入偏差,检测方法有两种
-
检测对旧MVCC对象版本的读取
-
在提交事务时检测是否有任何被忽略的写入现在已经被提交,如果有则终止事务并重试;如果其它提交的事务只包含查询,由于其不会产生写入偏差,则不中止事务
-
-
检测影响先前读取的写入
-
借助索引,如果事务中的UPDATE语句的WHERE条件列有索引,则事务提交时记录下该修改值,在其它事务也基于该WHERE条件进行更新时,则通知该事务所读取的数据已经不是最新的
-
-
-
性能
- 在SSI中,一个事务不需要阻塞等待另一个事务所持有的锁,与两阶段锁定相比,其查询延迟可预测更好;与串行执行相比,其性能不局限于单个CPU核的吞吐量