数据库事务的总结
一、数据库事务的概念
用一句话简单的说明:数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作(对数据库的相关增删改查的操作),要么完全地执行,要么完全地不执行。
二、数据库事务的四大特性
数据库的四大特性ACID,原子性、一致性、隔离性、持久性。每个特性都有其特定的职责。
2.1、原子性:一个事务中的所有操作,要不 操作全部成功,要不全部失败,不能存在中间态。
2.2、一致性:事务必须使得数据库从一个一致性状态转变到另一个一致性状态。比如银行转账,A账户转到B账户,不管转几次,A和B账户的总额不能变。
2.3、隔离性:是指多个用户同时请求数据库,开启多个事务同时处理某个数据库,隔离性保证了各个事务之间均不受干扰,每个事务都感觉不到其他事务的存在。
2.4、持久性:对数据库的修改是持久性的,一旦修改,就算数据库系统出现故障,这种修改也不会丢失,这点是数据库数据存放到硬盘中,并有redo log 和 binlog 一起保证的。
三、事务的隔离级别
隔离级别一共有四种:读未提交、读已提交、可重复读、串行化。这四种隔离级别分别可以解决的不同的问题,下面先描述一下不使用隔离级别可能会出现的几种问题。
(1)丢失修改
A和B两个事物同事修改同一个数据,A修改的提交在B提交之后,导致B好像没有修改,丢失修改。
(2)脏读
B事务修改了一个数据并未提交,A事物读取了这个数据,然后B事务回滚了,最后A又读取了一次,两次读取的数据不一致,称为脏读。
(3)不可重复读
A事务读取了一个数据后,B事务修改了这个数据,A事务又读取了这个数据,两次读取的数据也不一致,称为不可重复读。
(4)幻读
A事务更新了某个字段(范围是整个数据表的)(以id=1为条件的),B事务又插入了一条新的记录,导致A事务认为自己没有完全更新过来,就像出现幻觉一样。
针对这几种错误分别设置不同的隔离级别来解决:
第一种丢失修改一般使用加锁锁来解决,因此串行化可以解决,并且串行化可以解决上面出现的所有问题。
第二种问题脏读是因为读取其他事物未提交的数据,因为设置读已提交隔离级别可以解决这个问题。但不可解决不可重复读和幻读的问题。
第三种问题不可能重复读,是因为B事物的修改影响了A事务的读取数据,设置可重复读隔离级别,使得B事务修改数据和A事务读取数据互不影响,隔离开来,从而解决这个问题,同时解决 脏读问题。
第四种幻读问题,是因为A事务更新完数据后,B事务又插入了新的数据,设置串行化隔离级别可解决,并且这种隔离级别解决上面所有的问题。除了串行化,多版本并发控制(MVCC, Multiversion Concurrency Control)机制也可以解决该问题。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间(系统版本号,没开启新事物自动递增)和删除时间(当删除这行数据时,使当 前系统版本号作为删除时间)。这里存储的并不是实际的时间值,而是系统版本号 (可以理解为事务 的ID),每开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为 事务的ID.不同版本号的行记录相当于记录的一个快照,不同的事务在不同的快照上处理自己的操 作,互不影响。
SELECT时的规则要求
InnoDB会根据以下两个条件检查每行记录:
a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插 入或者修改过的.
b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
只有a,b同时满足的记录,才能返回作为查询结果.
引用:https://blog.csdn.net/whoamiyang/article/details/51901888。
四、事务的传播属性
spring通过配置事务的传播属性来严格控制事务行为。比如 在一个配置了事务的方法中调用了另一个方法,则另一个方法应该怎么运行,是新开启一个事务,还是和调用方法是一个事务,还 是 不开启事务?一共有七种传播属性。
- PROPAGATION_REQUIRED(需要) 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务。
- PROPAGATION_SUPPORTS (支持)表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行,如果不存在事务就不在事务中执行。
- PROPAGATION_MANDATORY (强制必须)表示该当前方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。
- PROPAGATION_REQUIRED_NEW(要求新事物) 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
- PROPAGATION_NOT_SUPPORTED(不支持新事物) 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
- PROPAGATION_NEVER (从不)表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。
- PROPAGATION_NESTED(嵌套)(spring) 表示如果当前方法已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。
五、分布式事务
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。当数据库的数据越来越大,数据库无法承受起压力,就会开始分库分表,分库分表后不同的库就会分布在不同的服务器上,明显的就会出现数据一致性问题。或者同一个事务中要调用的不同系统的不同的库,也要保证要不全都成功,要不全部回滚。
如何解决分布式事务数据一致性问题,下面分别总结几种方案。
(1)两阶段提交
事务协调器首先会将prepare消息写入日志中,然后向AB数据库发出prepare消息,操作完成后先不提交,而是返回yes,如果事务协调器收到的返回都是yes,则全部执行提交。如果有一个不 是yes,则向所有的执行器发送abort(退出、回滚)操作。
这种两阶段提交的方式能够保证数据一致性。但是牺牲了一定的可用性。
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景,如果分布式系统跨接口调用,目前 .NET 界还没有实现方案。
(2)三段式提交协议
在二阶段提交的基础上,在提交的时候,先发出precommit(预提交),当所有的执行器返回yes,在最终发出提交操作申请。否则向所有的执行器发送abort(退出)请求。
最大优点就是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。
缺点就是在去除阻塞的情况下引入了新的问题,那就是参与者接收到了PreCommit消息,然后网络出现问题,参与者和协调者无法通信,这种情况下,参与者依然会执行事务的提交。
(3)补偿事务(TCC)
简单举个例子说明,比如一个借钱产品,你发起借钱时,你的可借额度会被冻结,借钱成功,扣减额度并解冻。借钱失败则解冻额度,如果可借额度发生变化,还需要回滚额度。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC 定义及处理。
(4)本地消息表
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息 发 送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送 一个业务补偿消息,通知生产方进行回滚等操作。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
(5)基于可靠消息服务的分布式事务
有一些第三方的MQ是支持事务消息的,比如RocketMQ
- 在系统A处理任务A前,首先向消息中间件发送一条消息
- 消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。
- 消息中间件持久化成功后,便向系统A返回一个确认应答;
- 系统A收到确认应答后,则可以开始处理任务A;
- 任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它可以处理别的任务了。
- 但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。
- 消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;
- 当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。
原文:https://blog.csdn.net/u010425776/article/details/79516298
参考:https://blog.csdn.net/u012092620/article/details/80222007