存储层面的一致性保障
数据库事务
从工程的角度来讲, 应用层面的一致性是在数据库事务的基础上构建起来的。 事务型数据库有四个层面的要求 A (Atomic 原子性) C (Consistency 一致性) I ( Isolation 隔离性) D(Durablity 持久性)
在数据库层面 (A/I/D) 共同保证了C。
事务所解决的问题
在处理如资金交易这样需要严格保证正确性的时候可能会面临的问题,
一、 中间出故障了,导致部分执行成功部分执行失败, 原子性保障了这个问题不会出现, 所有任务要不成功要么失败
二、 并发问题, 多个请求同时操作一份数据, 导致超卖, 隔离性保障了这个问题不会出现, 所有的任务看起来是串行执行的。
三、数据丢失了, 持久性的特性保障了这一点不会发生
解决了 原子性、隔离性、持久性的问题, 才能保证交易的安全可靠, 这就是所谓的事务。
数据库事务保障的实现
原子性
从原子性的定义来讲, 需要保证所有执行要么都成功, 要么都失败。 如果进行到一半的过程中失败了, 必然需要补偿的机制, 去恢复原来的状态。 以Mysql为例, 其实现是以Undo Log的方式来实现的。 每个操作执行后会生成一条对应的Undolog,
隔离性
隔离的最终要求是让事务串行的执行, 从定义上来讲, 最有效解决隔离的方法就是全局串行, 允许数量为1的吞吐, 但这必然会带来性能的大幅度下降, 在生产过程中是不可被接受的。 一般定义了四种隔离级别
- 脏读脏写, 未提交的读可以被看到
- 读已提交
- 可重复读
- 串行化
讨论隔离性的问题的时候,我们思考问题的框架是两个或多个进程同时读写DB, 这个时候会发生什么事情。
并发读场景下面临的问题及解决
读已提交
脏读脏写会面临的问题是读到一个中间态的数据,
- 其中脏读会导致读到一个中间态的数据, 这个数据可能在事务结束的时候又变了。 这会导致某些场景下逻辑处理异常
- 脏写面临的问题是会写出一个不一致的结果, 如下图中的例子, 会发现 订单和发票的信息不一致了。
读已提交通过维持数据在更新前 更新后的两个状态, 解决了上述的问题
可重复读
针对于类似于下述的场景, 读已提交会读到一个中间态的数据。 Alice会读到少了100元这个状态。 从Read Commit的方式来继续往下推导,解决这种问题也比较简单, 我们记录每一个事务开始前的状态, 这个就是MVCC的机制
并发写场景面临的问题及解决
解决脏写问题
两个进程同时写一个对象, 会出现脏写问题, 导致最终状态的不一致, 如下图。
针对于这个问题解决方法也比较简单, 增加一个锁就可以解决, 写数据前需要先获取对应的锁,才能写。 为了控制锁的粒度, 一般会使用行锁
解决写丢失的问题
即使有了锁的保障, 两个进程同时写一份数据, 也会面临覆盖的问题, 比如一个进程SET 数据为A , 另一个进程Set数据为B, 那么就会面临写丢失的问题。
解决写丢失问题可以从两个层面来解决,
第一应用层面,显示的保证读写顺序, 通过SELECT FOR UPDATE、原子操作、 COMARE AND SET 等机制来解决
第二数据库层面, 自动检测到这种冲突之后将事务Abort, 但这个依赖于不同数据库的实现,
幻读问题导致写倾斜
两个进程的执行顺序都是先检查 后写,但写的操作有改变了原来的检查的条件,就会出现幻读问题。 导致单个进程执行的时候条件满足, 最后两个进程同时执行完结果条件不满足了。
问题定义了解决这种问题的方案也比较确定, 通过显示的加锁可以解决。
串行化
由于上面的弱隔离性有各种各样的问题, 串行化 便成了最终的解决方式, 让所有事务看起来是串行执行的。 实现串行化的方式有三种
- "真" 串行:通过存储过程, 单进程的执行事务, 对于IO密集型操作这个耗时比较大。 一个典型的应用场景是Redis的lua脚本
- 两阶段锁, 读任务获取共享锁, 写任务获取排他锁
- SSI 一种乐观锁的实现, 在可重复读的前提下增加乐观锁的机制,保障任务的执行。
总结来说, 对于读数据库采用了MVCC的机制来解决并发的问题, 对于写, 数据库提供了锁的机制,但需要在应用层面去处理。
持久性
持久性的问题比较简单, 对于单机DB 更多的是通过Redo log 和binlog的方式来解决, 对于主从架构更多的是同步到其他从服务来解决持久性的问题。
主从架构下的最终一致性保障
主从架构下主要考虑的问题是主从复制的问题。对于一致性要求比较高的场景下, 一般会做同步提交的方式,数据同步到从库之后才会commit本地事务。 但同步提交并不解决主从同步延迟的问题。 这个同步到从库的时间节点可能仅仅是日志同步。
应用层面的一致性保障
分布式事务的应用场景
- 跨数据库
- 跨服务
分布式事务的模式
XA
依赖于数据库提供的基础能力, 两阶段提交。 保持强一致性需要有锁或者MVCC的机制。
TCC模式
Try -> Confirm -> Cancel
Try阶段 资源尝试锁定
Confirm阶段 资源执行
Cancel阶段 补偿
发起者依赖本地事务 下游服务需实现幂等 这个是一个比较常用且容易实现的场景
Saga模式
逐级调用, 也需要一个任务协调者? 要不然实现起来很奇怪
事务队列
要点:
- 消息要有两个状态 A 已发送 B 已提交
2.消息系统扫描已发送未提交的任务调用上游服务进行确认