事务
一、事务的概念
事务(Transaction) 是一种机制,是一个最小执行单元,可以由一个或多个SQL语句组成。在同一个事务当中,所有的SQL语句都成功执行时,整个事务成功,只要有一个SQL语句执行失败,整个事务就都执行失败,因此事务是一个不可分割的工作逻辑单元。
二、事务的特性(ACID)
(1)、原子性(Atomicity)
事务的各个元素是不可分的,事务是作为一个整体提交或回滚,要么全部成功,要么全部失败。
(2)、一致性(Consistency)
事务执行的前后,要保持数据的完整性和一致性。
(3)、隔离性(Isolation)
各个事务之间是互相隔离的,互不干扰。
(4)、持久性(Durability)
一旦事务被提交,事务对数据所做的任何变动都会被永久地保留在数据库中。
三、事务的原理
数据库会为每一个客户端都维护一个空间独立的缓存区(回滚段),一个事务中所有的增删改语句的执行结果都会缓存在回滚段中,只有当事务中所有SQL语句均正常结束(commit),才会将回滚段中的数据同步到数据库。否则无论因为哪种原因失败,整个事务将回滚(rollback)。
四、事务并发问题(读问题)
(1)、脏读
A事务读取了B事务未提交的更改数据。
意思是:B事务更新了一份数据,此时A事务读取了这一份更新的数据,由于某种原因B事务回滚了,所以A事务读取的数据是"脏数据"。
时间 | 转账事务A | 取款事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 取出500元,余额500元 | |
T5 | 查询账户余额为500元 | |
T6 | 回滚事务,余额恢复1000元 | |
T7 | 转入100元,余额600元 | |
T8 | 提交事务 |
(2)、不可重复读
A事务读取了B事务已提交的更改数据。
意思是:同一个事务中,前后两次查询的数据不一致,原因是在两次查询数据之间,有另外一个事务对该数据进行了更新并提交了。
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 转出500元,余额500元 | |
T6 | 提交事务 | |
T7 | 查询账户余额为500元 (查询的结果和T4不一致) |
(3)、幻读
A事务读取了B事务已提交的新增数据。
意思是:同一个事务中,前后两次查询的数据笔数不一致,原因是在两次查询数据之间,有另外一个事务新增了(或者删除了)几条数据,并提交了。
时间 | 统计金额事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计账户余额为1000元 | |
T4 | 新增一个存款账户,存款500元 | |
T5 | 提交事务 | |
T6 | 查询账户余额为1500元 (幻读/虚读) |
五、丢失更新(写问题)
(1)、第一类丢失更新
A事务回滚时,把已经提交的B事务的更新数据覆盖了。(回滚丢失)
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 转入500元,余额1500元 | |
T6 | 提交事务 | |
T7 | 取出500元,余额500元 | |
T8 | 回滚事务 | |
T9 | 余额恢复1000元(回滚丢失了500元) |
(2)、第二类丢失更新
A事务覆盖B事务已经提交的数据 ,造成B事务所做操作丢失。(覆盖丢失)
时间 | 转账事务A | 取款事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出500元,余额500元 | |
T6 | 提交事务 | |
T7 | 转入500元 | |
T8 | 提交事务 | |
T9 | 余额为1500元(覆盖丢失,银行亏了500) |
六、事务的隔离级别
√:已解决
×:未解决
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(READ UNCOMMITTED) | × | × | × |
读已提交(READ COMMITTED) | √ | × | × |
可重复读(REPEATABLE READ) | √ | √ | × |
串行化(SERIALIZABLE) | √ | √ | √ |
从上到下,隔离级别越来越大。隔离级别越大安全性越高,同时效率也越低。
Oracle、SQL Server 默认隔离级别是读已提交,Mysql的默认隔离级别是可重复读。
MySQL实际工作使用读已提交。
所有隔离级别都不允许第一类丢失更新发生。解决丢失更新的办法就是加锁。
悲观锁:(多写场景)
认为多个事务更新操作一定会发生丢失更新。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。
悲观锁的实现:
- 传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
- Java里面的同步 synchronized关键字的实现。
悲观锁的分类:
-
共享锁(shared locks):读锁,简称 S 锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
-
排他锁(exclusive locks):写锁,简称 X 锁。如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,其他锁包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。
mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update
语句,加共享锁可以使用select ... lock in share mode
语句。
乐观锁: (多读场景)
认为多个事务更新操作不一定会产生丢失更新。总是假设最好的情况,每次读取数据的时候都认为不会有其他线程会更改数据,但在更新前需要判断一下在此期间有没有其他线程对数据进行了更改。
乐观锁的实现
-
版本号控制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
-
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
七、MySQL中事务简单使用
#1、开启事务
start transaction;#开启事务机制
set autocommit=0;#关闭自动提交
set autocommit=1;#开启自动提交
#2、编写一组逻辑sql语句
注意:sql语句支持的是insert、update、delete
[设置回滚点,可选项]
savepoint 回滚点名;
#3、结束事务
提交:commit;
回滚:rollback;
回滚到指定的地方: rollback to 回滚点名;
撤销回滚点: RELEASE SAVEPOINT 保存点名;
银行转账案例:
#A 账户给 B 账户转账。
#1.开启事务
START TRANSACTION;|setAutoCommit=0;#禁止自动提交 setAutoCommit=1;#开启自动提交
#2.事务内数据操作语句
UPDATE ACCOUNT SET MONEY = MONEY-1000 WHERE ID = 1;
UPDATE ACCOUNT SET MONEY = MONEY+1000 WHERE ID = 2;
#3.事务内语句都成功了,执行 COMMIT;
COMMIT;
#4.事务内如果出现错误,执行 ROLLBACK;
ROLLBACK;
注意:开启事务后,执行的语句均属于当前事务。