事务的ACID特性
1.Atomicity(原子性):
一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。
事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
以银行转账为例:
update t_balance set 余额=余额-1000 where card_id='111';
update t_balance set 余额=余额+1000 where card_id='222';
id为111的用户给id为222的用户转账1000,更新111的账户余额为原余额-1000,更新222的账户余额为余额+1000.这两个操作应该视为一个事务,要么都执行成功,要么都不执行成功,不能第一个操作成功了,第二个操作没有成功。如果第一个操作成功,第二个失败了,数据库应该回滚执行成功的第一个操作,让它回到未执行事务之前的状态
如果事务中的操作是只读的,那么要保持原子性很简单,一旦发生任何错误,要么重试,要么返回错误代码,因为只读并不修改数据库,但是如果是对数据库做更新操作,那么如果操作失败,很可能引起数据库系统状态的变化,因此必须保护系统中并发用户访问受影响的部分数据
2.Consistency(一致性):
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
例如:在一个表中有一个字段为姓名,该字段设为唯一约束,即表中不能有重复姓名,如果一个事务对姓名这个字段做了修改,但是在事务提交或回滚之后,,表中的名字变得非唯一了,这就破坏了事务的一致性要求,即事务将数据库从一种状态变成了一种非一致性状态。因此,事务是一致性的单位,如果事务中某个动作失败了,系统可以自动撤销事务——返回初始化的状态
3.Isolation(隔离性):
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
4.Durability(持久性):
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。这里的故障仅仅针对数据库的故障,如果是一些外部的原因,如:RAID卡损坏,自然灾害等导致数据库出问题,那么数据库的所有数据都有可能丢失。所以持久性只是保证事务系统的高可靠性,而非高可用性。
事务的隔离级别
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
READ-UNCOMMITTED:读未提交,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读)。
READ-COMMITTED:读已提交,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读)。
REPEATABLE-READ:可重复读,默认级别,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读)。
SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
锁问题
通过锁定机制可以实现事务的隔离性要求,使得事务可以并发操作,锁提高了并发,却也带来了不少问题
1.脏读
要了解脏读,首先要理解脏数据。所谓脏数据,就是指事务对缓冲池中的行记录的修改,并且没有提交。脏读是指在不同事务下,当前事务可以读到另外事务未提交的数据,简单来说,就是读到了脏数据。以下是一个脏读的例子:
time | 用户A | 用户B |
1 | set @@tx_isolation="read-uncommited"; | |
2 | set @@tx_isolation="read-uncommited"; | |
3 | begin; | |
4 | mysql>select * from t; **********1 row********* a:1 1 row in set(0.00sec) | |
5 | insert into t values(2) | |
6 | mysql>select * from t; **********1 row********* a:1 **********2 row********* a:2 2 row in set(0.00sec) |
这里首先将数据库的事务隔离级别进行了更换,在用户A的事务没有提交的情况下,用户B的两次查询操作得到了不同的结果,他读到了脏数据,违反了事务的隔离性。
2.不可重复读
不可重复读是指一个事务内多次读取同一数据集合。在这个事务没有结束时,另一个事务也在访问同一数据集合,并做了一些DML即更新操作,因此,在一个事务的两次读取同一事务期间,由于第二个事务的修改,第一个事务两次读取到的数据有可能会不一致,这种情况为不可重复读。
不可重复读和脏读的区别在于:脏读读到的是未提交的脏数据,而不可重复读读到的是已经提交的数据,但是其违反了数据库事务一致性的要求。下面通过一个例子显示不可重复读:
time | 用户A | 用户B |
1 | set @@tx_isolation="read-commited"; | |
2 | set @@tx_isolation="read-commited"; | |
3 | begin; | begin; |
4 | mysql>select * from t; **********1 row********* a:1 1 row in set(0.00sec) | |
5 | insert into t values(2) | |
6 | commit | |
7 | mysql>select * from t; **********1 row********* a:1 **********2 row********* a:2 2 row in set(0.00sec) |
这里首先将数据库事务的隔离级别设置成了读已提交。这个例子跟脏读的例子区别仅仅在于用户B的第二次查询是在用户A的事务提交后的数据。没有发生脏读现象,可是在用户B的事务还没有提交额情况下他两次读取的数据不一致。
一般来说,不可重复读是可以接受的,毕竟读到的数据时提交的,本身不会有太大问题,因此很多数据库厂商都经数据库事务的默认隔离级别设为READ COMMITED即读已提交。
3.幻读
幻象问题(Phantom Read),是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。事实上,InnoDB在它的默认级别REPEATABLE-READ下就可以避免幻读现象,不需要更高隔离级别的串行化。很多人可能以为实现串行化的代价比实现可重复读要高很多,其实不然,即使选用REPEATABLE-READ也不会有什么性能的损失,即使选择串行化,用户也不会得到性能的大夫提升。
4.丢失更新
丢失更新就是一个事务的更新操作会被另一事务的更新操作锁所覆盖,从而导致数据不一致例如:
1)事务T1将行记录r更新为v1,但事务T1未提交;
2)与此同时,事务T2将行记录r更新为v2,事务T2未提交;
3)事务T1提交;
4)事务T2提交;
但是在数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新,哪怕是最低级别的读未提交,对于行的DML操作会给行或更粗粒度的对象如页或表加锁,所以在2)中,事务T2不能修改,会阻塞至T1提交为止。
但是在实际的应用场景中,并不是那么回事,它会出现以下情况:
1)事务T1查询一行数据,放入本地内存,并显示给一个终端用户A
2)事务T2查询同一条数据,放入本地内存,并显示给另一个终端用户B
3)用户A修改这条数据,更新数据库并提交;
3)用户B修改这条数据,更新数据库并提交。
这个时候用户A的更新会被用户B的更新所覆盖。想象一个实际应用场景,在银行转账中,假如一个用户账号有10000余额,他用两个客户端进行转账操作,他先向某用户转账9000元,由于网络缘故需要等待,这时他用另一个客户端转账1元,两个操作完成后,第一个操作更新的账户余额1000被第二次更新的余额9999所覆盖了。
要避免丢失更新,就要让事务在这种情况下变成串行化。在步骤1)中加排他锁。
锁
悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
乐观锁一般来说有以下2种方式:
使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加 1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致,如果不一致就不修改,这样就实现了乐观锁。