事务
事务指的是一个执行单元,而在数据库中一个执行单元一般就是指的一个或多个sql语句,事务只有在提交之后才会真实生效。
事务操作分为:开启事务->执行语句->【回滚事务,提交事务】
开启事务:通过begin语句标识开启一个事务,开启时候后的SQL语句将作为一个执行单元,最后统一提交或者回滚;
执行语句:开启事务后具体的执行操作语句;
回滚事务:回滚事务操作和提交事务操作是一个互斥操作,如果执行了回滚则不能提交,执行了提交将不能回滚,回滚操作将丢弃本次事务的执行语句,回到开启事务最开始的状态;
提交事务:使执行单元生效,将操作持久化到数据库中;事务提交方式分为手动提交和自动提交
- 手动提交:手动通过语句指定提交时机,如果没有提交,则操作将保存在内存中会造成操作操作丢失。
- 自动提交:自动提交实际也是手动提交,只是由框架或者工具给封装了一层通过检测到你执行完成,由框架自动提交或者回滚,如Spring框架的自动提交回滚事务。
流程如下图所示:
代码如下面所示:
-- 提交事务 begin; insert into ts_user values(1,'张三',16); insert into ts_user values(2,'李四',15); insert into ts_user values(3,'赵六',18); commit; -- 执行后ts_user中将会有三条记录 -- 回滚事务 begin; insert into ts_user values(4,'王4',18); rollback; -- 最后使用rollback回滚后的事务,王4不会被插入到表中
事务4大特性
事务具有4大特性,分别是原子性、一致性、隔离性、持久性。
- 原子性Atomicity:一个事务中的所有操作,要么全部完成,要么全部不完成,最小的执行单位。
- 一致性 Consistency:事务执行前后,都处于一致性状态。
- 隔离性 Isolation:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不-致。
- 持久性 Durability:事务执行完成后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务4大隔离级别
事务的4大隔离是实际中使用事务的时候多用户在开启事务访问同一个数据库的时候之间的关系,隔离特性。
按照并发度由低到高排序为:串行事务->可重复度->读已提交->读未提交;
按照安全读由低到高排序为:读未提交->读已提交->可重复度->串行事务;
读未提交(Read Uncommited)RU:读尚未提交事务的数据,在begin后操作的数据也是可以被读取的;
读已提交(Read Committed)RC:读已提交的数据,执行了commit的数据;
可重复读(Repeatable Read)RR:可重复度,在当前事务中读取的数据永远是一致的,不会造成不一致,如,开启事务的时候读取id=1的name为a,则在整个事务中读取id=1的记录永远是一致的;
串行事务(Serializable):不支持并发,只有在上一个事务执行结束后才能开启新的事务执行,不允许多个事务同时操作数据库;
事务隔离级别解决了什么问题
在前面说到了事务隔离级别是在多用户访问同一个数据库的时候为了保证数据的完整性、正确性而提出的4大隔离级别,主要具体解决的是三个问题:(1)脏读、(2)不可重复读、(3)幻读。这4大隔离级别具体的实现方式在各个数据库有一定差异,但是目标都是解决这三个问题。
有如下数据
| 1 | 张三 | 16 |
|---|---|---|
| 2 | 李四 | 15 |
| 3 | 赵六 | 18 |
脏读:
脏读指的是读到了脏数据,也就是该数据并不是有效数据。例如小明下午说给你一百万,你接收到了这个消息高兴了半天立马去会所用了一你一个月工资请他happy了一晚上,然后第二天小明说不给你了,这时候你获得小明给你一百万的消息就是脏数据,还导致你花了一个月工资,这就是脏读导致的危害。
在事务中的体现就是,事务A在执行DML语句的时候事务未提交的前提下,事务B读取到了A未提交的数据,此时A进行回滚后B读取的数据就是错误的数据,造成脏读。
事务A执行:
begin; insert into ts_user values(4,'王5',11); -- 不提交事务B执行:
select * from ts_user;最后B查询到了(4,'王5',11)的记录,造成了脏读,因为此时事务A还未提交,此次操作随时会回滚导致4不插入到表里面,如果B来做某种业务逻辑判断,则会出现错误。
事务隔离级别读已提交就是为了解决脏读问题。
测试样例,读未提交问题:
会话A使用默认事务隔离级别,执行不提交事务的语句
-- 事务A mysql> select @@tx_isolation; -- 查看当前事务隔离级别 +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) mysql> begin;insert into ts_user values(4,'王5',11); -- 不提交会话B使用默认事务查询
-- 事务B mysql> use test; Database changed mysql> select * from ts_user; +----+------+------+ | id | name | age | +----+------+------+ | 1 | 张三 | 16 | | 2 | 李四 | 15 | | 3 | 赵六 | 18 | +----+------+------+ 3 rows in set (0.05 sec) mysql>可以看到,此时事务B查询结果并没有(4,'王5',11)这条记录;
对会话B设置隔离级别为未提交,再执行查询结果:
-- 事务B mysql> set session transaction isolation level read uncommitted; -- 设置本次回话隔离级别为未提交 Query OK, 0 rows affected (0.00 sec) mysql> select @@tx_isolation; -- 查看当前事务隔离级别 +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED | +------------------+ 1 row in set, 1 warning (0.00 sec) mysql> select * from ts_user; +----+------+------+ | id | name | age | +----+------+------+ | 1 | 张三 | 16 | | 2 | 李四 | 15 | | 3 | 赵六 | 18 | | 4 | 王5 | 11 | +----+------+------+ 4 rows in set (0.03 sec) mysql>此时会话B查询结果就有(4,'王5',11)这条记录了,但是此时事务A进行回滚操作。
-- 事务A mysql> rollback;事务B此时查询结果将看不到(4,'王5',11)这条记录,导致脏读。
以上就是造成脏读的例子,在多个事务中,一个事务执行语句后未提交,如果事务隔离级别为未提交,则可以读取到未提交事务的数据,但是未提交的事务随时可能因为各种原因回滚等操作,导致数据无法落盘,从而造成脏读问题。
在事务隔离级别中,除了事务读未提交隔离级别,其它隔离级别都解决了脏读的问题。
不可重复读
不可重复读指在同一个事务中多次读取的数据不一致。例如小明下午说给你一百万,你接收到了这个消息高兴了半天立马去会所用了一你一个月工资请他happy了一晚上,然后第二天小明说只给你一百块了,这时候你获得小明给你一百万和实际给你一百块的数据不一致,不能重复读取,导致你花了一个月工资,这就是不可重复读的危害。
在事务中的体现就是,事务A在执行DML语句的时候事务未提交的前提下,事务B读取到了A未开启事务前的数据,此时A进行提交,事务B再次发现不再是一开始的数据。
测试样例,不可重复读问题:
会话A使用默认事务隔离级别,执行不提交事务的语句
-- 事务A mysql> select @@tx_isolation; -- 查看当前事务隔离级别 +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) mysql> begin;insert into ts_user values(4,'王5',11); -- 不提交事务B读取ts_user表的数量,读取到事务B
-- 事务B mysql> set session transaction isolation level read committed; -- 设置本次会话隔离级别为读已提交 Query OK, 0 rows affected (0.00 sec) mysql> use test; Database changed mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select count(*) from ts_user; +----------+ | count(*) | +----------+ | 3 | +----------+ 1 row in set (0.21 sec)此时在事务B中,读取到ts_user表为3条记录;此时事务A提交事务
-- 事务A mysql> commit; Query OK, 0 rows affected (0.00 sec)事务B再次读取ts_user的数量
-- 事务B mysql> select count(*) from ts_user; +----------+ | count(*) | +----------+ | 4 | +----------+ 1 row in set (0.00 sec)可以看到事务B在没有结束的情况下读取到ts_user表的数量为4,造成了不可重复读。
如果事务B的事务隔离级别为可重复度读,则读取到的数据为3。
例子如下:
会话A使用默认事务隔离级别,执行不提交事务的语句
-- 事务A mysql> select @@tx_isolation; -- 查看当前事务隔离级别 +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) mysql> begin;insert into ts_user values(4,'王5',11); -- 不提交事务B读取ts_user表的数量,读取到事务B
-- 事务B mysql> set session transaction isolation level read committed; -- 设置本次会话隔离级别为可重复度 Query OK, 0 rows affected (0.00 sec) mysql> use test; Database changed mysql> begin; -- 开启事务 Query OK, 0 rows affected (0.00 sec) mysql> select count(*) from ts_user; +----------+ | count(*) | +----------+ | 3 | +----------+ 1 row in set (0.21 sec)此时在事务B中,读取到ts_user表为3条记录;此时事务A提交事务
-- 事务A mysql> commit; Query OK, 0 rows affected (0.00 sec)事务B再次读取ts_user的数量
-- 事务B mysql> select count(*) from ts_user; +----------+ | count(*) | +----------+ | 3 | +----------+ 1 row in set (0.14 sec)可以看到事务B在没有结束的情况下读取到ts_user表的数量为3,避免了可重复读。
上面举的例子说明不可重复读的危害,它对应的事务隔离级别是读已提交,已提交事务A对事务B产生的影响,这个影响叫做“不可重复读”,一个事务内相同的查询,得到了不同的结果。
该事务隔离级别已经满足了大多数的场景需求,市面上大多数其它数据库也是默认采用的该隔离级别,如Oracle、SQLServer等。但是MySQL默认的隔离级别是更加高一层的可重复读隔离级别。
ps:如果熟悉java的volitle关键字的就知道,不可重复读就是保证变量的可见性问题
幻读
幻读和不可重复读的现象类似,指的在同一个事务下多次读取数据结果不一致。例如小明下午说给你一百万,你接收到了这个消息高兴了半天立马去会所用了一你一个月工资请他happy了一晚上,结果你就美滋滋的等待一百万入账了,其实小明昨晚说的给你一百块,昨晚你幻听了,这就是幻读,导致你花了一个月工资,这就是幻读的危害。
从上面分析,幻读和不可重复读的现象和结果都是一致,但是这两个之间的隔离级别还是有差异;
幻读强调的读取的insert操作,不可重复读强调的update、delete、select操作。
如幻读一个经典例子在于:事务A执行插入ID=4操作未提交,事务B读取到事务A开启事务前的数据,事务B检测到未插入数据,则准备插入ID=4,此时A已经提交,B再插入导致duplicate key。
再啰嗦依据,幻读是强调的是insert,这也是为什么事务隔离级别有串行事务隔离级别的原因。
下面会分析为什么串行事务隔离级别能够解决幻读
测试样例,幻读问题:
会话A使用默认事务隔离级别,执行不提交事务的语句
-- 事务A mysql> select @@tx_isolation; -- 查看当前事务隔离级别 +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) mysql> begin;insert into ts_user values(4,'王5',11); -- 不提交事务B读取ts_user表的id=4的数据,结果为Empty set,空集合。
-- 事务B mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from ts_user where id = 4; Empty set (0.03 sec)此时A事务提交insert
-- 事务A mysql> commit; Query OK, 0 rows affected (0.00 sec)事务B执行插入,出现了重复Key的错误
-- 事务B mysql> insert into ts_user values(4,'王5',11); ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'
以上测试样例的现象和不可重复读的现象一样,都是无法知道其它事务已经插入了这条id=4的数据,从而造成的异常,可重复读隔离级别也只能保证一个事务中多次读取的记录一致,但是并不能保证发现其它事务已经操作过的数据,因此无法解决幻读问题。
为什么串行隔离级别能够解决幻读问题?
前面说到串行隔离级别不允许多个事务操作同一个数据库,这也就是说在上面案例中事务A开启了事务后,只有事务B只有等待事务A提交后才能提交,它们之间就形成了串行关系,类似java里面的synchronized。
案例:
会话A使用默认事务隔离级别,执行不提交事务的语句
-- 事务A mysql> select @@tx_isolation; -- 查看当前事务隔离级别 +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) mysql> begin;insert into ts_user values(4,'王5',11); -- 不提交事务B读取ts_user表的id=4的数据,结果被阻塞无法获取到。
-- 事务B mysql> select * from ts_user where id = 4; -- 被阻塞此时只有事务A结束后,事务B才能操作id=4的数据;
事务A执行提交
-- 事务A mysql> commit; Query OK, 0 rows affected (0.00 sec)提交的同时,事务B会取消阻塞继续执行下去,并查询到id=4的记录
-- 事务B mysql> select * from ts_user where id = 4; +----+------+------+ | id | name | age | +----+------+------+ | 4 | 王5 | 11 | +----+------+------+ 1 row in set (0.61 sec)
这样就解决了幻读问题,解决了插入重复key的问题。再次强调,幻读和不可重复读现象一致,但是它们强调的重点不一样,幻读强调的是插入,不可重复读强调的读取和更新删除操作,主要隔离级别的可重复度和串行就是为了解决这两个问题的。
ps:可重复读的原理是MVCC保证了可重复读取,串行隔离级别能够解决幻读问题的原理是加锁,这个在后面的文章进行分析。
总结
数据库有4大隔离级别,分别是,读未提交、读已提交、可重复读、串行;这4大隔离级别主要解决了脏读、不可重读、幻读的问题;读已提交解决了脏读问题,可重复读解决了不可重读问题,串行事务隔离界别解决了幻读问题。隔离级别越高支持并发能力越差,数据越安全。
MySQL的默认隔离级别为可重复读,其它数据库默认隔离级别是读已提交。
MySQL可重复读原理是MVCC,串行事务的原理是加锁。