脏写: 事务一修改了某一个值,但是尚未提交,事务二又修改了这个值,并提交,这就是脏写,也就是后写的操作覆盖了较早的写入
解决: 一般是延迟后一个事务的写操作,实现中采用行锁
脏读: 事务一修改了某个值,但是尚未提交,事务二读取到了事务一修改后的值,后面如果事务一回滚了,那么事务二就读取到了一个错误的值
解决: 对每个更新对象,数据库都会保存一个旧值和当前持锁事务的新值两个版本,提交之前,其他事务读取旧值,提交之后,用新值替换旧值
不可重复读(读倾斜): 事务一读取了数据A,之后根据查询结果做了操作,过程中事务二修改了数据A,并提交了事务,后面事务一又读了一次数据A,发现值不一样了
解决: 数据库保存对象多个不同的提交版本,也就是多版本并发控制(MVCC)
问题: MVCC如何支持索引:
1. PostGreSQL:把同一个对象的不同版本放到同一个内存页面上
2. BTree结构的,采用写时复制技术,需要更新时,不修改现有的页面,总是创建一个新的修改副本,拷贝必要的内容, 然后让父节点,或者递归向上知道树的root节点,都指向新的节点
那些不受更新影响的页面都不需要复制,保持不变并被父节点所指向
并发写冲突: 多个事务同事更新一个对象,导致的更新丢失,比如在递增计数器和账户的时候
解决方案: 1. 原子写: 例如set value = value+1, 通常采用对读取对象加独占锁的方式来实现,这种技术有时被称为游标稳定性,另一种实现方式是强制所有的原子操作都在单线程上执行
2. 显示加锁: for update 指令指示数据库对返回的所有结果行要加锁
3. 自动检测更新丢失: 数据库完全可以借助快照级别隔离来高效的执行检查,PostGreSQL的可重复读,Oracle的可串行化以及SQL Server的快照级别隔离,都可以自动检测
何时发生了更新丢失,然后会终止违规的事务。但是Mysql/Innodb的可重复读却并不检测更新丢失。
幻读(写倾斜):可以看作更广义的更新丢失,两个事务读取相同的一组对象,然后更新其中一部分;不同事务可能更新不同的对象,则可能发生写倾斜;二不同对象如果更新的是同一个对象
则可能发生脏写或更新丢失
解决: 1. 显示加锁: for update 指令指示数据库对返回的所有结果行要加锁
2. 串行化: 保证即使事务可能并行执行,但最终的结果与每次执行一个即串行化执行结果相同。
产生条件(模式):
1. 首先输入一些匹配条件,即采用SELECT查询所有满足条件的行
2. 根据查询的结果,应用层代码来决定下一步操作
3. 发起数据库写入(INSERT,UPDATE或DELETE)并提交事务,而这个写操作会改变步骤2作出决定的前提条件
换句话说,就是如果提交步骤3的事务,再重复执行步骤1的查询,就会返回完全不同的结果,这样就可能会导致幻读
串行化: 目前大多数可提供可串行化的数据库都是用了以下三种技术之一:
1. 严格按照串行顺序执行, ①H-Store,Redis和Datomic等采用串行化方式执行事务。可以避免锁开销,但是其吞吐量上限是单个CPU核的吞吐量。②存储过程 ,但是语言跟不上,缺乏函数库
2. 两阶段锁定: 第一阶段:执行之前获取锁,第二阶段:事务结束释放锁
解释: ①如果事务A已经读取了某个对象,此时事务B想要写入该对象,那么B必须等到A提交或终止才能继续,以确保B不会再事务A执行的过程中去修改对象
②如果事务A已经修改了对象,此时事务B想要读取该对象,则B必须等到A提交或终止才能继续,这样才不会出现读到旧值的情况
说明:两阶段加锁不是两阶段提交
实现: 1. 读写锁,也就是锁可以处于共享模式或独占模式
要读取对象,先获取共享锁,要求改对象,需要获取独占锁,可以从共享锁升级到独占锁,事务获得锁之后,一直持有到事务结束,
3. 乐观并发控制技术,例如可串行化的快照隔离
谓词锁: 作用类似于共享/独占锁,而区别在于,它并不属于某个特定的对象(如表的一行),而是作用于满足某些搜索条件的所有查询对象。
说明: 但是谓词锁的性能不佳:如果活动事务中存在很多锁,那么检查匹配这些锁就变得非常耗时。因此,大多数使用2PL的数据库实际上实现的是索引区间锁(next-key locking),本质上它是对谓词锁的简化或者近似。
如果没有合适的索引,那么数据会回退到对整个表施加共享锁。
系统容错的复制机制,常见的复制方案:
1. 主从复制:主节点承担数据写入,从节点在各自节点上维护数据的备份副本。如果从主节点或者同步更新的从节点读取,则可以满足线性化。
2. 共识算法,共识算法需要解决复制问题,业余要一些措施来防止脑裂和过期的副本。
3. 多主复制(不可线性化):具有多主节点复制的系统通常无法线性化的,主要由于他们同时在多个节点上执行并发写入,并将数据异步复制到其他节点,因此他们可能会产生冲突的写入。
4. 无主复制(可能不可线性化): 对于无主节点复制的系统,取决于具体的quorum配置来决定是否保证线性化。
原子提交和两阶段提交:
原子提交:单节点上,事务提交非常依赖于数据持久写入磁盘的顺序关系:先写入数据,然后在提交记录。事务提交(或终止)的关键点在于磁盘完成日志记录的时刻:在完成日志记录写之前如果发生了崩溃,则事务需要中止;
如果在日志写入完成之后,即发生崩溃,事务也被安全提交,这就是在单一设备上(某个特定的磁盘连接到一个特定的点)实现原子提交的核心思路。
两阶段提交:两阶段提交是一种在多接地那之间实现事务原子提交的算法,用来确保所有节点要么全部提交,要么全部中止。她是分布式数据库中的经典算法之一,2PC在某些数据库内部使用,或者以XA事务形势(例如Java Transaction API)
或SOAP Web服务WS-AtomicTransaction的形式提供给应用程序。
两阶段提交分为准备阶段和提交阶段,事务开始时,两个事务分别在两个节点上写数据,写数据完成后进入准备阶段,他们告诉协调者准备完成,然后进入提交阶段,所有节点反馈可以提交之后,完成提交。
分布式事务处理:
1. Exactly-once消息
2. XA交易: X/Open XA是异构环境下实施两阶段提交的一个工业标准。目前许多传统数关系数据库包括PostGreSQL,Mysql,SqlServer Oracke和消息队列(ActiveMQ,MSMQ,IBM MQ)都支持XA
XA并不是一个网络协议,而是一个与实物协调者进行通信的C API。当然他也支持其他语言的API绑定,例如JavaEE中,XA事务是由Java事务API来实现。
分布式事务的限制:
1. 如果协调者不支持数据复制,而是在单节点上运行,那么他就是整个系统的单节点故障。
2. 许多服务器端应用程序都倾向于无状态模式,而所有的持久状态都保存在数据库中,这样应用服务器可以轻松地添加或删除实例。但是当协调者就是应用服务器的一部分时,部署方式就发生了根本变化。
突然间,协调者的日志成为可靠系统的重要组成部分,它要求与数据库本身一样重要,这样应用服务器已经不再是无状态。
3. 由于XA需要与各种数据系统保持兼容,他最终其实是多系统可兼容的最低标准。例如,无法深入检测不同系统之间的思索条件,不适用于SSI