1. 数据库事务是什么
事务的定义,已经有太多文章写过,我就不重复了。我理解的事务就是用来保证数据 *** 作符合业务逻辑要求而实现的一系列功能。换句话说,如果数据库不支持事务,上面业务系统的程序员就需要自己写代码保证相关数据处理逻辑的正确性。而数据库事务就是把一系列保证数据库处理逻辑正确性的通用功能在数据库内实现,并且尽量提高效率。
举个例子,数据库最开始普及就是在金融业,银行的存取款场景就是一个最典型的OLTP数据库场景,而事务就是设计用来保证类似场景的业务逻辑正确性的。
![事务的四个基本特性](https://img-blog.csdnimg.cn/2967b3d9484c405289d711ef5bf69890.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAd2luZHRhbGtlcnd5,size_20,color_FFFFFF,t_70,g_se,x_16)
**原子性**,如果你要给家人转账,必须在你的账户里扣掉100块,在家人账户里加上100块,这两笔 *** 作需要一起完成,业务逻辑才是正确的。但是程序在做修改的时,肯定会有先后顺序,试想一下程序扣了你的钱,这个时候程序崩溃了,家人账户的钱没有加上。那这100块是不是消失了?你是不是要发疯?那么,就把这两笔 *** 作放进一个事务里,通过原子性保证,这两笔 *** 作要么都成功,要么都失败。这样才能保证业务逻辑的正确性。
**一致性**,有很多文章讲过一致性,但是很多人会把一致性跟原子性混在一起说。事务的一致性指的是指每一个事务必须保证执行之后所有库内的规则依旧成立。比如内外键,constraint,触发器等。举例来说,你在储蓄卡里有100元,理财账户里有100元,基金账户有100元,那么你在资产总和里会看到300元,这个300元必须是其他三个账户余额加在一起得到的。你在给家人转帐100元是从储蓄卡里转出去了100元,那么在数据库上可以通过创建触发器的方式,当储蓄卡余额账户减100元的同时,把资产总和也同步减去100,不然的话,就会出现逻辑上的错误,因为你已经转走了100块储蓄卡余额,实际资产总和应该是200,如果还是300,数据库状态就不一致了。所以实现事务的时候,必须要保证相关联的触发器以及其他所有的内部规则都执行成功,事务才能算执行成功。如果在减去资产总时出错,那么这笔转帐交易也不能成功。因为这样数据库就会进入不一致的状态。
那么这里跟原子性的区别到底在哪里呢?原子性是指个多个用户指令之间必须作为一个整体完成或失败,而一致性更多是数据库内的相关数据规则必须同时完成或失败。
**持久性**,最容易理解的一个,事务只要提交了,那么对数据库的修改就会保存下来不会丢了。简单来说,只要提交了,数据库就算崩溃了,重启之后你刚存的100块依然在你的账户里。
**隔离性**,每个事务相对于其他的事务是有一定独立性的,不能互相影响。因为数据库需要支持并发的 *** 作来提高效率。在并发 *** 作时,一定要通过 *** 作之间的隔离来保证业务逻辑的正确性。比如,你转帐100块给家人,一系列 *** 作的最后一步可能是输入验证码,这个时候转帐还没有完成,但是在数据库里你的账户对应的记录中已经减去100块,家人账户也加了100块,就等着验证码输入以后,事务提交,完成 *** 作。那么,这个时候,家人通过手机银行能够查到这100块么?你的答案可能是不能,因为这样才符合业务逻辑,因为你的转帐 *** 作还没有提交,事务还没有完成。那么数据库就应该保证这两个并发 *** 作之间具有一定的隔离性。
那么到底应该隔离到什么程度呢?隔离性又分为4个等级:由低到高依次为Read uncommitted(读未提交)、Read committed(读提交)、Repeatable read(可重复读取)、Serializable(序列化),这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。这些东西是什么意思?请有兴趣的小伙伴自行百度,很多文章都写的很清楚。
那么怎么理解不同的隔离等级呢,首先要理解并发 *** 作,并发 *** 作就是指有不同的用户同时对一个数据进行读、写 *** 作,那么在这个过程中,每个用户应该看到什么数据才能保证业务逻辑的正确性呢? 如果是前面存取款的场景,我必须看到的是已经存进来的钱,也就是必须是已经提交的事务。而12306刷火车票呢,你可以看到有10张余票,但是在下单的时候告诉你票卖完了,因为同时有10个用户把票买掉了,你需要重新刷余票,这个也是可以接受的,也就是说我可以读到一些虚假的余票,这样在业务上也没有什么问题。那么在设计这两个不同系统时,就可以选择不同的事务隔离级别来实现不同的并发效果。不同的隔离等级就是要在系统的并发性和数据逻辑的严谨性之间做出的平衡。
2. 数据库如何实现事务
数据库实现事务会有多种不同的方式,但基本的原理类似,比如都需要对事务进行统一的编号处理,都需要记录事务的状态(是成功了还是失败了),都需要在数据存储的层面对事务进行支持,以明确哪些数据是被哪些事务、插入、修改和删除的。同时还会记录事务日志等,对事务进行系统化的管理以实现数据的原子性,一致性和持久性。
要实现事务的隔离性,最基础的就是通过加锁机制把并发 *** 作适当的串行化来保证数据 *** 作的正确逻辑。但是为了要保证系统具有良好的并发性能,必须要在实现事务隔离性时需要找到合理的平衡点。大部分数据库(包括Oracle,MySQL,Postgres在内)在做并发控制的时候都会采用MVCC(多版本并发控制)的机制来保证系统具有较高的并发性,不同数据库实现MVCC的具体方案也不尽相同,但其基本原理类似。
3. MVCC实现原理
所谓MVCC,就是数据库中的同一查询根据相关事务执行的先后顺序以及隔离级别的不同,可能会存在不同版本的结果,通过这样的手段来保证大部分查询 *** 作不会被修改 *** 作阻塞并保证数据逻辑的正确性。也就是数据库通过保存多个版本的数据( 历史 数据)来提高系统的并发查询能力。简单来说就是用存储空间来交换并发能力。下面以Postgres为例介绍一下MVCC的一种实现方式帮助大家理解这个重要的数据库概念。通过下面的图来解释Posrgres里最基本的数据可见性是如何实现多版本控制的。
![在这里插入图片描述](https://img-blog.csdnimg.cn/fd880f62b48d4a43986fa9c7d1a79e4e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAd2luZHRhbGtlcnd5,size_20,color_FFFFFF,t_70,g_se,x_16)
首先,Postgres里的每一个事务都有编号,这里可以简单理解为时间顺序编号,编号越大的事务发生越晚。然后,数据库里的每一行记录都会保存创建这条记录的事务号(Cre),也会在记录删除时保存删除这条记录的事务号(Exp),换句话说,只要Exp这里一列里记录了事务编号,就说明这条记录被删除了。那么一个事务应该能看见那些记录呢?Postgres里每一个事务都会保存一个当前系统的事务快照(Snapshot),这个快照里会保存事务创建时当前系统的最高(最晚)事务编号,以及目前还在进行中的事务编号。那么如上图所示的一个事务的快照里最高事务编号为100,目前正在进行的事务有25,50和75。那么对应左边数据记录,这6行数据的可见性就如同标注的一般:
第一行,Cre 30,没有删除,在100这个时间点,应该能看到。
第二行,Cre 50,没有删除,但是50这个事务还没有提交,正在进行中,所以看不见。
第三行,Cre 110,没有删除,但是100这个时间点110事务还没有发生,所以看不见。
第四行,Cre 30,Exp 80,在80的时候数据被删掉了,所以看不见。
第五行,Cre 30,Exp 75,在30的时候被创建,75时候被删掉了,但是75这个事务在100的时候还没有提交,所以这条记录在100的时候还没有删掉,所以看得见。
第六行,Cre30,Exp 110,在30的时被创建,110时候被删掉,但是在100时候,110还没有发生,所以看得见。
综上,就是这个事务对这六条记录的可见性,也就是一个数据版本。那么大家可以看一下如果另一个事务的快照里存的是最高事务编号为110,正在进行的事务为50,那么它能看到的数据应该是哪几行呢?同时大家也看到,Postgres里删除一行数据其实就是在这一行的Exp这个列记录一个删除事务的编号,相当于做了一个删除标记,而数据没有真正被删除,因此Postgres数据库需要定期做数据清理 *** 作(Vacuum)。Pstgres的在现实场景里会比这里介绍的要复杂,因为我们这里假定所有的事务最终都是正确提交了,如果存在某些事务没有提交的情况,那么可见性就会更加复杂,这里不再展开了。
数据库事务是基本的数据库概念,之前已经有很多很好文章做过介绍,这里希望能把自己的理解用比较通俗的描述分享给大家,欢迎来讨论交流。
1、首先使用链接服务器,在一台服务器上链接另外一台服务器2、启用事务
3、在事务内,分别 *** 作两台服务器的表数据
4、遇错回滚事务,或最后提交事务
以上4步即可实现在不同主机的数据库服务器支持事务
1、什么是分布式事务
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的 *** 作由不同的小 *** 作组成,这些小的 *** 作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小 *** 作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
2、分布式事务的产生的原因
2.1、数据库分库分表
当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个 *** 作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。
2.2、应用SOA化
所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行 *** 作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。
以上两种情况表象不同,但是本质相同,都是因为要 *** 作的数据库变多了!
3、事务的ACID特性
3.1、原子性(A)
所谓的原子性就是说,在整个事务中的所有 *** 作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的 *** 作都会被回滚,整个事务就像从没被执行过一样。
3.2、一致性(C)
事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
3.3、隔离性(I)
所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。
3.4、持久性(D)
所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。
4、分布式事务的应用场景
4.1、支付
最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些 *** 作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的 *** 作必然需要引入分布式事务。
4.2、在线下单
买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。
5、常见的分布式事务解决方案
5.1、基于XA协议的两阶段提交
XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:
总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
5.2、消息事务+最终一致性
所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地 *** 作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:
1、A系统向消息中间件发送一条预备消息
2、消息中间件保存预备消息并返回成功
3、A执行本地事务
4、A发送提交消息给消息中间件
通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地 *** 作步骤二出错,则整个事务失败,不会执行A的本地 *** 作步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地 *** 作+发消息)+B系统的本地 *** 作,其中B系统的 *** 作由消息驱动,只要消息事务成功,那么A *** 作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地 *** 作,如果本地 *** 作失败,消息会重投,直到B *** 作成功,这样就变相地实现了A与B的分布式事务。原理如下:
虽然上面的方案能够完成A和B的 *** 作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。
5.3、TCC编程模式
所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个 *** 作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。
6、总结
分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)