事务
什么是事务?
事务 是一组操作的集合,是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销请求,这些操作要么同时成功,要么同时失败
一组操作集合,不可分割,一起向系统提交/撤销,要么同时成功,要么同时失败
例子:
如果a向b转账100元,总共分为三个步骤
(1)检查a的手机余额是否>=100元
(1)a的手机账户钱扣掉100元
(2)b的手机账户增加100元
这三步过程合起来就是一个事务,一旦a的手机账户扣掉100元后,系统出现问题,那么b的手机账户就收不到100元,那么这100元究竟去了哪里,所以把这三步过程当作一个事务,一旦系统出现问题,那么就会将刚刚的扣钱操作撤销,也就是回滚事务,这样保证了要么一起成功,要么一起失败,不会出现100元不知道去哪了
为了解决这种问题,需要通过数据的事务来完成,业务开始时开启事务,执行完成后提交事务,一旦过程出现报错问题,就回滚事务,将数据恢复到原始状态。
你可要注意了,mysql中事务是自动提交的,执行一条语句,就会立刻提交事务。
事务的操作
好的下面我们就开始利用代码细致的分析一下事务操作
drop table if exists account;
create table account(
id int primary key AUTO_INCREMENT comment 'ID',
name varchar(10) comment '姓名',
money double(10,2) comment '余额'
) comment '账户表';
insert into account(name, money) VALUES ('张三',2000), ('李四',2000);
正常操作
# 模拟使用三条语句构成一个事务
# 正常执行完张三1900元,李四2100元
-- 查询张三余额
select * from account where name='张三';
-- 张三的余额减少100元
update account set money=money-100 where name='张三';
-- 李四的余额增加100元
update account set money=money+100 where name='李四';
恢复余额操作
-- 恢复金额
update account set money=2000 where name='张三' or name='李四';
异常操作
中间插入一句报错的语句
-- 查询张三余额
select * from account where name='张三';
-- 张三的余额减少100元
update account set money=money-100 where name='张三';
出错误啦 你的语句出问题了
-- 李四的余额增加100元
update account set money=money+100 where name='李四';
最后卡在报错语句这个位置,余额增加的语句没有执行
继续回滚
-- 恢复金额
update account set money=2000 where name='张三' or name='李四';
上面的例子解决需要用到下面的方法
控制事务1
- 查看/设置事务提交方式
select @@autocommit;
set @@autocommit=0;
- 提交事务
commit ;
- 回滚事务
rollback ;
控制事务1方法修改了事务自动提交方式,改为手动提交,每次写完sql语句,需要执行commit进行一个手动的提交
控制事务2
- 开启事务
start transaction/begin;
- 提交事务
commit ;
- 回滚事务
rollback;
还是利用上面那个转账案例
利用控制事务1的方法
出错啦:
-- 事务中出现报错
-- 控制事务1
-- 设置为手动提交
set @@autocommit=0;
-- 查询张三余额
select * from account where name='张三';
-- 张三的余额减少100元
update account set money=money-100 where name='张三';
出错啦
-- 李四的余额增加100元
update account set money=money+100 where name='李四'
# 出错后进行回滚事务
rollback;
一旦卡在出错啦的位置,你不提交事务(commit),此时数据库中的数据不会变化,然后你执行rollback;进行回滚操作
事务内部没有错误:
-- 控制事务1
-- 设置为手动提交
set @@autocommit=0;
-- 查询张三余额
select * from account where name='张三';
-- 张三的余额减少100元
update account set money=money-100 where name='张三';
-- 李四的余额增加100元
update account set money=money+100 where name='李四';
commit ;
最后手动commit提交后,数据库中的数据张三余额才会变成1900,李四的余额变成2100
利用控制事务2的方法
出错啦:
-- 开启事务
-- 控制事务2
-- 注意此时是自动提交(@@autocommit=1)
start transaction ;
-- 查询张三余额
select * from account where name='张三';
-- 张三的余额减少100元
update account set money=money-100 where name='张三';
出错啦
-- 李四的余额增加100元
update account set money=money+100 where name='李四';
-- 异常进行回滚事务
rollback ;
由于使用了start transaction来开启一个事务,所以必须要手动的commit来提交事务,否则数据库中的数据是不会发生变化的,如果事务内部出现错误,那么就可以使用rollback来进行回滚事务
没有错误:
-- 开启事务
-- 控制事务2
-- 注意此时是自动提交(@@autocommit=1)
start transaction ;
-- 查询张三余额
select * from account where name='张三';
-- 张三的余额减少100元
update account set money=money-100 where name='张三';
-- 李四的余额增加100元
update account set money=money+100 where name='李四';
-- 正常提交事务
commit ;
此处为正常提交事务,事务内部没有错误,开启一个事务后,开始执行三个步骤,最后提交事务,此处开启一个事务就意味着你后面要进行手动提交事务,类似与那个设置手动提交事务
事务的四大特性(ACID)
-
原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
解释:类似与刚刚转账案例,三个步骤是一个整体,要么全部失败,要么全部成功,不会出现张三的余额减少100后发生报错,导致李四的余额也没有增加,这就导致二者钱的总数与原来不一样 -
一致性(Consistency):事务完成时,必须使所有的数据保持一致状态。
解释:也就是转账三步骤完成后,二者总的钱是不变的 -
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
后面会细致解析 -
持久性(Durability):事务一旦提交或回滚,对数据库中的数据改变时永久的
解释:由于一旦提交或者回滚后,数据就真实存放到了电脑物理磁盘中,肯定数据改变是永久
并发事务问题
(1)脏读:一个事务读到另外一个事务没有提交的数据
解释:例如两个并发的事务a和b,其中事务A查完数据库中的一条记录后,事务a继续修改了一条记录,此时事务A并未提交,此时并发事务b读取了事务a修改的记录,这就导致事务b读取到事务a未提交的数据
(2)不可重复读:一个事务先后读取同一条记录,但是两次读取的数据不同
解释:例如两个并发事务a和b,其中事务a读取数据库中的一条记录后,事务b对数据库的这条记录进行修改后,提交事务b,此时事务a继续读取这条记录,发现和上次读取的数据不一样
(3)幻读:一个事务按照条件查询数据,没有对应的数据行,准备插入数据时,发现这行数据存在
解释:例如两个并发事务a和事务b,其中事务a读取了id=1的数据时,发现没有这条记录,然后事务b插入id=1的数据,并且提交了事务b,此时事务a准备插入id=1的数据时发现已经存在这条数据,因为插入数据时报错,显示有这条记录
事务隔离级别
(1)查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION;
(2)设置事务隔离级别
SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED |
READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
事务隔离级别越高,数据越安全,性能越低
事务隔离级别
隔离界别 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|
read uncommitted | 不可避免 | 不可避免 | 不可避免 |
read committed | 可避免 | 不可避免 | 不可避免 |
repeatable read(默认) | 可避免 | 可避免 | 不可避免 |
Serializable | 可避免 | 可避免 | 可避免 |
- read uncommitted
- 脏读(不能解决)
1、先开启一个cmd终端,设置当前会话的事务隔离级别为read uncommitted,查询余额都是2000,然后开启一个事务a,查询此时的数据
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 2000.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 2000.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
2、再次开启一个cmd终端,开启一个事务b,修改数据
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set money=money-100 where name='张三';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
3、此时事务a的终端查询数据,数据发生变化,查到了事务b未提交的数据,也就是脏读
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1900.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
4、此时事务b经过commit提交后,继续度,数据仍然时1900和2000
- read committed
- 脏读(解决)
1、首先打开一个cmd终端,设置事务隔离级别为read committed,查询数据后,开启一个事务a
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1900.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
2、打开第二个cmd终端,开启一个事务b,然后修改数据
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set money=money-100 where name='张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
3、在事务a的终端里查数据,发现数据没有变化
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1900.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
4、在事务b的终端里提交数据
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
5、在事务a的终端里查询数据,发现数据发生变化,也就是这种隔离级别避免了脏读
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1800.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
- 不可重复读(不能解决)
1、首先开一个cmd终端,然后开启一个事务a,查询数据
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1800.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
2、再开启另一个cmd终端,开启一个事务b,然后修改数据后,提交事务b
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set money=money-100 where name='张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
3、在事务a的终端中查询数据,发现同一个事务中刚刚查的数据和现在查的不一致
- repeatable read(默认)
- 脏读(解决)
- 不可重复读(解决)
1、首先打开一个cmd终端,设置事务隔离级别repeatable read,然后开启一个事务a,查询数据
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1600.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
2、再开另外一个终端,然后开启一个事务b,然后修改数据后提交事务b
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set money=money-100 where name='张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
3、再次查询事务a数据,数据不变
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1600.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
4、将事务a提交,然后查询,发现数据变化
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1500.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
- 幻读
1、首先开启一个cmd,然后开启一个事务a,查询id=3的数据发现没有
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account where id=3;
Empty set (0.00 sec)
2、然后再次开启一个终端,开启一个事务b,插入id=3的数据,提交事务b
mysql> insert into account(id,name,money)values(3,'王五',2000);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
3、因为事务a刚刚发现没有id=3的数据,准备插入数据发现报错,继续查id=3的数据发现查不到,很奇怪,见鬼了
mysql> insert into account(id,name,money) values('3','haha',2000);
ERROR 1062 (23000): Duplicate entry '3' for key 'account.PRIMARY'
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1500.00 |
| 2 | 李四 | 2000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
- Serializable
- 脏读(解决)
- 不可重复读(解决)
- 幻读(解决)
1、首先开启一个cmd终端,然后设置隔离级别为 Serializable,然后开启一个事务a,查询id=4的数据
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+---------+
| id | name | money |
+----+--------+---------+
| 1 | 张三 | 1500.00 |
| 2 | 李四 | 2000.00 |
| 3 | 王五 | 2000.00 |
+----+--------+---------+
3 rows in set (0.00 sec)
2、然后开启另外一个终端,然后开启一个事务b,准备插入id=4的数据发现卡住
mysql> insert into account(id,name,money)values('4','nihao',2000);
3、在事务a的终端插入id=4的数据,结果事务b的终端报错
mysql> insert into account(id,name,money) values(4,'da',2000);
Query OK, 1 row affected (0.00 sec)
mysql> insert into account(id,name,money)values('4','nihao',2000);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql>
到此,mysql基础篇已经全部学完