事务的ACID中AID是手段,C是目的
原子性,隔离性,持久性是因,是手段,而一致性是由于事务遵照了原子性,隔离性,持久性所呈现的结果。
单数据源的一致性
当服务只使用一个数据源时,通过AID来获得一致性是最经典的做法,也是相对容易的。
外部一致性
当一个服务使用多个数据源,甚至多个服务涉及多个不同的数据源,这种多数据源的一致性问题,称为“外部一致性“。
对于外部一致性:在可承受下尽量获得强度大的一致性保障
在单个服务使用单个数据源的场景下的事务保证一致性
事务的一致性依赖于数据源,开发人员最多只能对接口做一层标准化的包装(JDBC接口),并不能深入参与到事务的运作过程中。
事务的开启,终止,提交,回滚,甚至是隔离级别,全部都要依靠底层数据源的支持。
数据库如何通过原子性,隔离性和持久化实现事务的一致性
数据库面临的原子性问题
原子性即事务要么生效,要么不生效,不会存在中间状态。
但是数据先存在内存上(断电全无),存在将数据写入磁盘的过程(中间状态,破坏了原子性)。
具体来说存在两种情况:
- 未提交事务,但是已经写入数据到磁盘,此时崩溃。因为没有提交事务,所以重启后需要将磁盘中的数据恢复成没有修改过的样子。
- 已提交事务,但是还没将数据写入磁盘,此时崩溃。因为提交了事务了,所以重启之后需要将未写入的数据写入磁盘。
崩溃无法避免,只能通过将事务修改的数据提前备份从而实现恢复
通过提交日志(Commit logging)(也叫做redo log)实现提前备份从而保证了原子性
在事务从内存写入磁盘之前,先将事务内容写入提交日志中并且持久化到磁盘,这样崩溃的时候也能通过提交日志,看到这个事务到底是提交了没有,这个事务的内容是什么。
崩溃恢复中,如果事务没有写入提交日志提交标志,那么这个事务就是提交失败的,需要回滚,如果写入提交标志,那么就是提交成功的。
提交日志策略在日志写入磁盘之前不允许数据写入磁盘,造成了资源浪费
STEAL:对于事务提交和数据写入硬盘的顺序:允许数据先日志写入称为steal,不允许称为NO-STEAL,Commit Logging就是NO-STEAL
Write-Ahead Logging:通过undo-log实现STEAL
当数据写入磁盘前先写入undolog,如果崩溃后,按照undolog对提前写入的数据变动进行擦除。
完整的Write-Ahead Logging在崩溃恢复
- 分析阶段:找出没提交但是写入的数据,找出提交了但是没有写入的数据。
- redo阶段:从checkpoint开始,将提交了的事务的数据都写入磁盘中。
- undo阶段:将没提交的事务的数据都撤销(通过undolog记录的undo值)
数据库由于并发导致隔离性问题,通过锁解决
隔离性保证了各个事务各自读,写的数据互相独立,不会彼此影响。
但是数据库并发执行,为了保证隔离性就引入了锁。
现代数据库使用三种锁实现了各种隔离级别,隔离级别是因为使用的锁不同而导致的
- 写锁:数据加上写锁,只有持有写锁的事务才能修改数据,而且也不能添加读锁。
- 读锁:多个事务可以对一个数据添加多个读锁,加读锁后就不能加写锁了,但是仍然能被读取。
- 范围锁:相当于对一个范围(不是一组数据)加了写锁了。eg. select * where price < 100 for undate;
只有需要修改数据时才加写锁,只有读数据的时候才会考虑加读锁(读未提交不加读锁)。
隔离级别1:串行化
给所使用的数据加三种锁,即可实现串行化。通过两阶段锁来加锁。
隔离级别2:可重复读
给事务涉及的数据加读锁和写锁,且一直持有直到事务结束,但是不加范围锁。
此时就会出现范围查询的结果在一个事务中不同,因为没有加范围锁。
隔离级别3:读已提交
给事务涉及的数据加的写锁持续到事务结束,但是加的读锁在查询语句结束后会立即释放。
这样的话,T1修改了数据a,T2只有等T1事务提交后T1释放了写锁,才能给数据a添加读锁进行读操作。
所以称为读已提交。
隔离级别4:读未提交
只会对事务涉及的数据加写锁,在事务结束后释放。
所以多个事务并发,想读就读。
幻读,不可重复读,脏读,都是一个事务读的过程,其他事务写造成的
针对这个问题,提出了MVCC(针对读-写冲突的优化)
新老版本共存,从而达到读数据时不加锁的目的。
数据库中的每一行数据,都记录了两个看不见的字段:create_version , delete_version,记录的是事务ID。
只写入版本数据行,不删除数据。
- 插入数据时,添加数据作为新的一版本行,在这条数据的create_version记录插入的事务ID,delete_version为空。
- 删除数据时,复制数据作为新的一个版本行,delete_version为事务ID,create_version为空。
- 修改数据时,当作”删除旧数据,插入新数据的组合“,添加两个版本行,一个按删除,一个按插入数据。
通过隔离级别来确认读取哪个版本行:
- 可重复读:读取craete_version<=事务ID的最新数据行
- 读已提交:读取最新版本即可
单个事务使用多个数据源下的事务一致性
分布式事务下的两阶段提交实现事务一致性
准备阶段
协调者询问所有事件源准备好提交了吗(对于数据源,就是日志持久化完成,数据持久化完成,就差一条commit recode了)
提交阶段
所有参与者都回复了可以提交了,协调者发送提交命令,完成提交。
缺点
任何一个参与者失败则全部回滚。
网络必须没有问题。
所有参与者的执行事务必须等待最终命令才能提交,最终命令之前会加锁。
三阶段提交实现事务一致性
CanCommit,PreCommit,DoCommit
会在执行分布式事务之前,询问各数据源能否执行事务。
避免了有些数据源本身根本就不想执行事务,还需要其他数据源参与一轮回退。
缺点和两阶段相同
多个服务使用一个数据源
因为一个服务集群中数据库的压力总是最大的,所以首先就不推荐使用。
这种场景,使用消息队列,让数据库作为消息队列的消费者来消费事务。
分布式事务
在分布式系统中CAP只能满足两个:
强一致性:多节点数据任何时刻完全相同
可用性:消息同步阶段服务就不可用了
分区容忍性:网络通信不是永远可靠的
分布式事务机制:通过消息队列实现最大努力交付:将消费放到消息队列,失败就重复直到成功
没有回滚的概念,只能成功,或者失败(需要人工介入)。
事务的消费者会在执行事务过程中一直显示”消费中“。
将多个数据源的消费消息传递给消息队列,在消息队列中,每个消息会不断去消费源重复消费(通过UUID确保幂等)直到成功。
只有当多个数据源的数据消费都完成后才会将事务调整者撤去”消费中“,变为一致性。
缺点
会因为不能回滚,发生超卖(一个物品卖给多个用户,多个用户显示”消费中“),需要人工介入处理。
分布式事务机制:TCC(类似银行家算法,在消费前预估能否消费并且加锁)
通过卖出商品前给商品加锁,其他用户不能购买,从而无法超卖。
过程:
- try(获取资源,如果成功给资源加锁)
- confirm(如果try获取资源成功,执行事务)
- cancel(如果try获取资源失败,释放资源,事务失败)