一.事务四大特性:
- 原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么去不完成,要么均不执行。
- 一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序串行执行的结果一致。
- 隔离性(Isolation):事务的执行不受其它事务的干扰,事务执行的中间结果对其他事务必须是透明的。
- 持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不会被丢失,即使数据库出现故障。
上面这写概念可能对有些初学者来说优点晦涩难懂,下面我就通过举例子来说帮助大家理解一下。
1.原子性
假如张三欠李四1000元,张三发工资了,准备给李四还钱,他通过银行像李四转了1000元,但是在转出去的瞬间,银行断电了,那么请问张三到底还钱成功了吗?我们都知道,还钱这一事务包括了两步,第一步,从张三账户上减去1000元,第二步给李四账户上加上1000元,如果断电的瞬间从张三账户上减去1000元的操作完成了,而给李四账户上加前的这一操作还没有执行,那张三不是要报警了?怎样避免这种闹剧呢?这里就用到了事务原子性的特点,将从张三账户上减去1000元和给李四账户上加上1000元看做一步(一个原子),也就是不可分割的操作,这个操作要么成功,要么失败。
2.一致性
还是张三前李四1000元,张三没有足够的钱去还,怎么办呢?张三让他的母亲给李四还200,让他老婆给李四还300,让他爹给李四还300,让他爷爷给李四还100,自己给李四还100。对于还钱这个事务来说,张三一个人给李四还1000元和张三发动家人帮他给李四还1000元是一样的,达到了同样的效果(这个效果就是张三和李四之间的债务关系解除了),每个帮他还款的家人都是一个独立的事务,这些事务并行执行与张三一个人给李四还1000元这个事务,达到了同样的效果,这就是事务的一致性。
3.隔离性
还是还钱,张三给李四还款呢(也就是还款这个事务正在执行呢),王五也给李四还款呢(也就是王五与李四的还款这个事务正在执行呢),但是最后,李四只收到了1000元,那是谁还的呢?(假设不能查看转账记录)这时就出现问题了,如果这时其中一人说慌了,那最终就会可能会引发一个刑事案件(哈哈,开个玩笑)。如何避免这种问题的发生呢?隔离两个事务,要么张三先和李四进入还款事务,王五等待,要么王五和李四进入还款事务,李四等待。而在还款事务正在进行的过程种,等待的那一个人将被隔离在事务外。这就是事务的隔离性。
4.持久性
保证对数据库执行增,删,改操作完成的同时,数据被持久化到数据库,本地磁盘,备份数据库等各个灾容处理设备,不会造成数据的丢失。
二.事务的并发问题
- 读并发:脏读,不可重复读,幻读。
- 写并发:第一类丢失更新(回滚丢失更新),第二类丢失更新(提交丢失更新)。
三.读并发问题
1.脏读
A事务读取了B事务未提交的数据。
说明:对于事务隔离级别设置较低的数据库,其是允许将未提交数据写入到数据库的。但是,即使写入到数据库,若事务回滚,也是可以再将数据恢复为原数据的。
所以,可能发生脏读的现象:A事务修改了某数据,但是还未提交。此时,B事务读取了该数据。但此时A事务又发生了回滚。那么B事务读取到的就是个“不存在”的脏数据了。
2.不可重复读
读取可已提交的事务。A事务先读取了一个数据,而后B事务修改(update或delete)了该数据并提交。此时A事务再次读取该数据时,该数据已经被修改或不存在。即无法再读到原来相同的数据。
3.幻读
也叫虚读。读取了已提交的事务。与不可重复读不同的是,发生插入(insert)操作。A事务先进行了某一条件的检索操作,而B事务插入了若干数据并提交。这些数据存在符合A事务检索条件的数据。此时A事务再做相同的检索,其检索结果就会与第一次的不同。
四.事务隔离级别
为了防止读并发问题的发生,标准SQL定义了四个隔离级别。级别由底到高分别为:读取未提交,读取已提交,可重复读,串行化。随着隔离级别的提高,其防止并发的效果也是逐步提高,但是其系统开销也是逐步提高的,代码的执行效率是逐步降低的。
- 读未提交:不防止任何并发读问题。
- 读已提交:防止脏读问题,可能出现不可重复读与幻读。
- 可重复读:防止脏读和不可重复读,不能防止幻读。
- 串行化:不会发生任何并发问题。
- 知识点一:mysql默认的隔离级别是可重复读。
- 知识点二:oracle默认的隔离级别是读已提交。
五.并发写问题
第一类丢失更新:也称回滚丢失更新。A,B事务同时读取某数据,并均做修改。A事务先进行提交,而B事务又做回滚。此时,A事务提交的更新数据丢失。
第二类丢失更新:也称为提交更新丢失。A,B事务同时读取某事务,并均做修改操作。A事务先做了提交,然后B事务提交。此时A事务提交的更新数据会被B事务的提交给覆盖。
六.加锁机制
通过加锁可以解决并发写问题。锁可以分为两类:
乐观锁(Optimistic lock):每次访问数据时,都会乐观的认为其它事务此时肯定不会同时修改该数据。但是真正修改时,会在代码先判断数据是否已经被其他事务修改过了。所以锁是加在代码中的。
乐观锁如何解决并发写问题?
乐观锁是加在代码中的锁机制,一般充当乐观锁的有两类数据:版本号与时间戳。它们的工作原理是相同的。
A,B事务从DB中取数据时同时会读出一个数据版本号。当A事务将修改后的数据同步到DB中时,数据库的版本号会加1。当B事务发生回滚或覆盖时,会首先对比自己数据的版本号与DB的版本号是否相等。若它们相等,则说明DB中数据没有发生变化,B事务可以将数据回滚到原始状态,或将修改写入到DB中。若小于DB中的版本号,则说明其他事务已经修改过该数据,将抛出异常。
悲观锁(Pessimistic lock):每次访问数据时,都会悲观的认为其它事务一定会同时的修改该数据。所以其在访问数据时,会在数据库中先给数据加锁,以防止其他事务同时修改该数据。所以锁是加在数据库中的。
悲观锁如何解决并发写问题?
悲观锁是加载DB中的锁机制,又分为两种:
写锁(排它锁):当A事务对某数据加上排它锁后,A事务将独占该数据,可以对该数据惊醒读,写操作。但其他事务是不能在为该数据添加任何锁,知道A事务将排它锁解除,将数据释放。
使用方法:
在SQL语句中,若要为本次操作(事务)添加排它锁,则可以在正常的SQL语句最后添加上for update即可。例如:“select * from student where age<20 for update”。
读锁(共享锁):当A事务对数据加上共享锁后,只能对数据进行读操作。但其他事务也可以为该数据添加共享锁,读取该数据。但不能添加写锁,直到所有事务将其共享锁解锁,将数据释放,才能对数据添加排他锁。
使用方法:
在SQL语句中,若要为本次操作(事务)添加共享锁,则可在正常的SQL语句最后添加上lock in share mode即可。例如:“select *from student where age<20 lock in share mode”