18.7.23
一、概念:
作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。
事务是数据库运行中的一个逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。
二、ACID(原子性、一致性、隔离性和持久性)
1、原子性(Atomic):对于其数据修改,要么全都执行,要么全都不执行。
如果执行一半发生问题,则需要回滚。
在多事务并发执行中,即使没有发生问题,也可导致最终结果错误,因此还需要其他特性。
2、一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
例子1:拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
例子2:对银行转帐事务,不管事务成功还是失败,应该保证事务结束后ACCOUNT表中A和B的存款总额始终为2000元。如果一个人扣100元,一个人得50元,就破坏了一致性。
事务的一致性决定了一个系统设计和实现的复杂度。事务可以不同程度的一致性:
·强一致性:读操作可以立即读到提交的更新操作。
·弱一致性:提交的更新操作,不一定立即会被读操作读到,此种情况会存在一个不一致窗口,指的是读操作可以读到最新值的一段时间。
·最终一致性:是弱一致性的特例。事务更新一份数据,最终一致性保证在没有其他事务更新同样的值的话,最终所有的事务都会读到之前事务更新的最新值。如果没有错误发生,不一致窗口的大小依赖于:通信延迟,系统负载等。
·其他一致性变体还有:
·单调一致性:如果一个进程已经读到一个值,那么后续不会读到更早的值。
·会话一致性:保证客户端和服务器交互的会话过程中,读操作可以读到更新操作后的最新值。
3、隔离性(Isolation)
多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
事务并发的隔离性是可以通过隔离级别的设置来控制的。
由事务并发引起的三个问题(脏读、不可重复读、幻读)是可以通过隔离级别的调整来控制的。
4、持久性(Durability)
持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。即使出现了任何事故比如断电等,事务一旦提交,则持久化保存在数据库中。
三、由事务并发引起的5类问题
1、第一类丢失更新(Update Lost):此种更新丢失是因为回滚的原因,所以也叫回滚丢失。此时两个事务同时更新count,两个事务都读取到100,事务一更新成功并提交,count=100+1=101,事务二出于某种原因更新失败了,然后回滚,事务二就把count还原为它一开始读到的100,此时事务一的更新就这样丢失了。
2、脏读(Dirty Read):此种异常时因为一个事务读取了另一个事务修改了但是未提交的数据。举个例子,事务一更新了count=101,但是没有提交,事务二此时读取count,值为101而不是100,然后事务一出于某种原因回滚了,然后第二个事务读取的这个值就是噩梦的开始。
当事务隔离级别为read uncommitted的时候,一个事务执行到一半并未提交,另一个事务读到了数据,前一个事物回滚,这样第二个事务读到的就是不真实的数据。
3、不可重复读(Not Repeatable Read):此种异常是一个事务对同一行数据执行了两次或更多次查询,但是却得到了不同的结果,也就是在一个事务里面你不能重复(即多次)读取一行数据,如果你这么做了,不能保证每次读取的结果是一样的,有可能一样有可能不一样。造成这个结果是在两次查询之间有别的事务对该行数据做了更新操作。举个例子,事务一先查询了count,值为100,此时事务二更新了count=101,事务一再次读取count,值就会变成101,两次读取结果不一样。
此时他们的隔离级别都是read committed。
4、第二类丢失更新(Second Update Lost):此种更新丢失是因为更新被其他事务给覆盖了,也可以叫覆盖丢失。举个例子,两个事务同时更新count,都读取100这个初始值,事务一先更新成功并提交,count=100+1=101,事务二后更新成功并提交,count=100+1=101,由于事务二count还是从100开始增加,事务一的更新就这样丢失了。
5、幻读(Phantom Read):幻读和不可重复读有点像,只是针对的不是数据的值而是数据的数量。此种异常是一个事务在两次查询的过程中数据的数量不同,让人以为发生幻觉,幻读大概就是这么得来的吧。举个例子,事务一查询order表有多少条记录,事务二新增了一条记录,然后事务一查了一下order表有多少记录,发现和第一次不一样,这就是幻读。
MYSQL数据库的幻读是有概率性的,不好演示。
四、数据库事务隔离级别
数据库事务隔离级别可以影响以上问题。
数据库的隔离级别实现一般是通过数据库锁实现的。
1、读未提交(Read Uncommitted):该隔离级别指即使一个事务的更新语句没有提交,但是别的事务可以读到这个改变,几种异常情况都可能出现。极易出错,没有安全性可言,基本不会使用。
那么脏读、不可重复读、幻读都是有可能发生的
2、读已提交(Read Committed):该隔离级别指一个事务只能看到其他事务的已经提交的更新,看不到未提交的更新,消除了脏读和第一类丢失更新,这是大多数数据库的默认隔离级别,如Oracle,Sqlserver。
可避免脏读,不能避免不可重复读和幻读
3、可重复读(Repeatable Read):该隔离级别指一个事务中进行两次或多次同样的对于数据内容的查询,得到的结果是一样的,但不保证对于数据条数的查询是一样的,只要存在读改行数据就禁止写,消除了不可重复读和第二类更新丢失,这是Mysql数据库的默认隔离级别。
避免脏读、不可重复读,但是幻读还是有可能产生
4、串行化(Serializable):意思是说这个事务执行的时候不允许别的事务并发执行.完全串行化的读,只要存在读就禁止写,但可以同时读,消除了幻读。这是事务隔离的最高级别,虽然最安全最省心,但是效率太低,一般不会用。
避免所有异常读的产生
5、他们的安全性是递增的,效率性是递减的。
五、各种隔离级别对各异常的控制能力
六、数据库锁
一般可以分为两类,一个是悲观锁,一个是乐观锁。
悲观锁一般就是我们通常说的数据库锁机制,乐观锁一般是指用户自己实现的一种锁机制,比如hibernate实现的乐观锁甚至编程语言也有乐观锁的思想的应用。
悲观锁:顾名思义,就是很悲观,它对于数据被外界修改持保守态度,认为数据随时会修改,所以整个数据处理中需要将数据加锁。悲观锁一般都是依靠关系数据库提供的锁机制,事实上关系数据库中的行锁,表锁不论是读写锁都是悲观锁。
悲观锁按照使用性质划分:
1、共享锁(Share locks简记为S锁):也称读锁,事务A对对象T加s锁,其他事务也只能对T加S,多个事务可以同时读,但不能有写操作,直到A释放S锁。
2、排它锁(Exclusivelocks简记为X锁):也称写锁,事务A对对象T加X锁以后,其他事务不能对T加任何锁,只有事务A可以读写对象T直到A释放X锁。
3、更新锁(简记为U锁):用来预定要对此对象施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;当被读取的对象将要被更新时,则升级为X锁,主要是用来防止死锁的。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。
七、悲观锁按照作用范围划分:
行锁:锁的作用范围是行级别,数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。举个例子,一个用户表user,有主键id和用户生日birthday当你使用update … where id=?这样的语句数据库明确知道会影响哪一行,它就会使用行锁,当你使用update … where birthday=?这样的的语句的时候因为事先不知道会影响哪些行就可能会使用表锁。
表锁:锁的作用范围是整张表。
乐观锁:顾名思义,就是很乐观,每次自己操作数据的时候认为没有人回来修改它,所以不去加锁,但是在更新的时候会去判断在此期间数据有没有被修改,需要用户自己去实现。既然都有数据库提供的悲观锁可以方便使用为什么要使用乐观锁呢?对于读操作远多于写操作的时候,大多数都是读取,这时候一个更新操作加锁会阻塞所有读取,降低了吞吐量。最后还要释放锁,锁是需要一些开销的,我们只要想办法解决极少量的更新操作的同步问题。换句话说,如果是读写比例差距不是非常大或者你的系统没有响应不及时,吞吐量瓶颈问题,那就不要去使用乐观锁,它增加了复杂度,也带来了额外的风险。
八、乐观锁实现方式:
版本号(记为version):就是给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1,读取数据的时候把version读出来,更新的时候比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以你可以使用update … where … and version=”old version”这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。
时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间。
待更新字段:和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想。
所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。
九、乐观锁几种方式的区别:
新系统设计可以使用version方式和timestamp方式,需要增加字段,应用范围是整条数据,不论那个字段修改都会更新version,也就是说两个事务更新同一条记录的两个不相关字段也是互斥的,不能同步进行。旧系统不能修改数据库表结构的时候使用数据字段作为版本控制信息,不需要新增字段,待更新字段方式只要其他事务修改的字段和当前事务修改的字段没有重叠就可以同步进行,并发性更高。
参考:https://blog.csdn.net/aluomaidi/article/details/52460844
十、行为分类
不受保护的行为(unprotected actions):
不需要进行undone/redone
除了一致性,没有其他的ACID特性,他们的作用不可以依靠
例如,在事务期间对临时文件的操作
受保护的行为(protected actions):
可以也必须进行undone/redone
在结束之前不会显示出结果,具有完全的ACID特性
例如,数据库操作
真实的行为(real action):
不能被undone的操作
这些操作直接作用于物理世界,非常难或者几乎不可能进行撤销
ATM机取钱
十一、事务的模式
1、平面型事务(FLAT TRANSACTIONS)
计算的是本地的变量,无内部结构(只有逻辑,没有存档点),因此不会被DBMS看到。
且只能面向单一的DBMS,在同一时间只能运行一个事务,
使用调用或语句级别的接口访问DBMS,一般开始于Begin,接着是一系列操作,最后以COMMIT或者ROLLBACK结束。
只适用于简单的应用。一旦回滚,则将整个事务的所有操作进行回滚,所有在没有COMMIT之前的操作全部丢失
2、带存档点的平面型事务
平面型事务一旦回滚,整个事务的所有操作都要回滚。
存档点的作用就是,回滚的时候不需要逐条undo,可以直接从存档点开始undo。节省时间,用空间换时间。
存档点可以是多个,1,2,3。后面的依赖前面的,如果把存档点2释放了,则存档点3也自动释放。
savepoint将事务变成一系列的每一个都可以单独回滚的行为。
3、嵌套事务(NESTED TRANSACTIONS)
嵌套事务是一套有层次的操作,子事务的结果要依赖于它父亲事务的操作。
比较好理解,一个事务调用另一个被注解事务的service。
4、链式事务(Chained Txns)
多个事务顺序执行,一个事务COMMIT之后,另一个事务马上BEGIN,没有事务可以在两个事务之间对数据进行修改。必须串行执行。
和savepoint不同的地方在于:
COMMIT允许DBMS释放所有的锁
后面的子事务不能回滚到之前的子事务中去,所以该事务不具备原子性
4.1、批量更新问题
批量更新(bulk update)本身就是一个天然事务,如果一旦失败则全部回滚。
虽然链式事务模型解决了这个问题,但是它要求应用自己去解决维护数据库状态、解决失败操作这些问题。因为DBMS没有办法将整个事务回滚。
5、补偿事务(compensating transactions)
一种特殊类型的事务,用于将之前事务的操作结果进行消除。这种消除只能是逻辑层面的,而不是物理层面的。
不常用。
SAGE事务
由两个链式事务构成,分别是是T1,T2,…,Tn,和对应事务的补偿事务C1,C2,…,Cn。
事务的提交顺序只能是两种:
按顺序提交T1,…,Tn。
按顺序提交T1,…,Tj,Cj,…,\C1。
提交按链式提交,如果回滚,也按链式的C回滚。
参考:https://blog.csdn.net/u013007900/article/details/77927723
十二、service层与事务:DAO层执行的是对数据源的单个操作,service层比较适合作为事务的存放层。在service层中调用DAO层的多个方法,并且使用同一个链接对象。
参考:https://www.cnblogs.com/chentingk/p/5864194.html