原子性经典的表现是在火车票售卖:
数据库因为是网络服务,所以同一时间一定会有多个客户端访问
当还剩下一张票时,客户端A选择购买,还没有执行更新数据库时,客户端B检查了票数,发现还有一张票,于是也购买了火车票,执行数据库票数更新。然后A也进行票数更新。
这样就导致一张票卖给了两个人,这显然是不行的
从火车票售卖中,我们认识到,对于数据库数据的访问:
事务主要用于处理操作量大,复杂度高的数据。
事务其实就是一组DML语句集合,这些语句在逻辑上有一定的相关性。这一组DML语句,要么全部执行成功,要么全部失败。
比如,注销某社交平台的账号,该社交平台需要将账号信息,消费信息,发表的留言等都删除,这一系列删除的DML语句,就可以构成一个事务
同时,正如同上述所说的,数据库同一时间会有多个客户访问,也就会有多个事务。所以事务也需要满足4个属性
:
Read Uncommitted
),读提交(Read Committed
),可重复读(Repeatable Read
),串行化(Serializable
)这四个属性,简称为ACID
设计事务的本质是为了,简化应用程序访问数据库时的编程模型,我们不需要去顾忌潜在错误,并发问题,网络异常等。事务内的DML语句要么全部成功,要么全部失败。因此事务本质是为了应用层服务的
MySQL的一行信息,称为一行记录
在MySQL中,只有InnoDB
数据库引擎的数据库或表才支持事务,MyISAM等其他引擎不支持
查看隔离级别
select @@global.tx_isolation; --查看全局隔离级别
select @@session.tx_isolation; --查看当前会话隔离级别
select @@tx_isolation; --同session.tx_isolation
查看全局使用global.tx_isolation
查看当前,因为二者效果相同,所以建议使用短一点的tx_isolation
启动mysql客户端时,当前会话的隔离级别和全局相同
产生效果的是当前会话的隔离级别
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | 默认为可重复读
+-----------------+
设置隔离级别
设置全局的隔离性级别
mysql> set global transaction isolation level READ UNCOMMITTED;
mysql> select @@global.tx_isolation;
+------------------+
| @@global.tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
设置当前会话的隔离级别
mysql> set transaction isolation level READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
注意,更改当前会话隔离级别需要重启才会生效
事务的常见提交方式有两种:
语法:
begin
(或start transaction) 开启事务
事务的起点
commit
提交事务
事务的结束
rollback
回滚事务
配合savepoint使用
我们先将隔离性设置为读未提交
,方便查看结果
设置全局的隔离性级别,需要重启客户端
mysql> set global transaction isolation level READ UNCOMMITTED;
mysql> select @@global.tx_isolation;
+------------------+
| @@global.tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
准备如下表:
mysql> create table if not exists account(
-> id int primary key,
-> name varchar(30) not null default '',
-> blance decimal(10,2) not null default 0.0 comment '薪资'
-> )engine=InnoDB default charset=UTF8;
简单使用
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
mysql> savepoint s1;--第一个保存点 |
|
mysql> insert into account values (1,'张三',4000); | mysql> select * from account;
插入第一条记录。 | +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| +----+--------+---------+
mysql> savepoint s2;--第二个保存点 |
|
mysql> insert into account values (2,'李四',5000); | mysql> select * from account;
插入第二条记录。 | +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> rollback to s2; | mysql> select * from account;
回滚到s2,李四的记录没了。 | +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| +----+--------+---------+
|
mysql> rollback; | mysql> select * from account;
回滚到最开始,没有记录。 | Empty set (0.00 sec)
查看自动提交状态
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | 自动提交 开启
+---------------+-------+
可以使用set开启或关闭事务的自动提交
mysql> set autocommit=0;
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF | 自动提交 关闭
+---------------+-------+
mysql> set autocommit=1;
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | 自动提交 开启
+---------------+-------+
autocommit=0代表关闭自动提交
autocommit=1代表开启自动提交
其实,我们之前编写的单个DML语句就是一个事务
,只不过因为开启了自动提交,所以每个DML语句都会commit
接下来,我们进行验证
首先将自动提交关闭
set autocommit=0;
我们开启两个mysql客户端,一个进行insert,一个读取
可以看到,客户端1插入数据后,客户端2并没有看到新的数据,此时客户端1再commit,客户端2才能看到数据
事务操作注意事项
事务提交后,不可回滚
InnoDB
支持事务,MyISAM
不支持事务begin
开启事务,比较简短隔离性
。同时MySQL允许事务受到不同程度的干扰,就有了一种重要特征:隔离级别
隔离级别
读未提交【Read Uncommitted】
在该隔离级别,所有事务都可以看到其他事务没有提交
的执行结果,这相当于没有隔离性,会出现很多问题,比如:脏读,幻读,不可重复读
读提交【Read Committed】
该隔离级别是大多数数据库默认的隔离级别(不是MySQL默认的)。一个事务只能看到其他的已经提交
的事物的执行结果。这种隔离级别会出现不可重复读的问题,即多次select 出现不同的结果
可重复读【Repeatable Read】
该隔离级别是MySQL默认的隔离级别,它确保同一个事物,在执行中,多次读取操作数据时,看到同样的数据行,但会有幻读问题
串行话【Serializable】
该隔离级别是最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读问题,它在每个读的数据行上加上共享锁,但是可能会导致超时和锁竞争(效率低
)
隔离基本都是通过锁实现的,不同的隔离级别,锁的使用不同。常见的有:表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等
接下来介绍隔离级别间的效果差异
客户端B的事务均在客户端A的事务后启动
几乎没有加锁,虽然效率高,但是问题太多,非常不建议使用
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
| mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> update account set blance=4444 where id=1; |
| mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
可以看到,客户端A未提交的事务,其执行结果,客户端B可以看见
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但未commit得数据,这种现象就叫做脏读
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
| mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> update account set name='zhangsan' where id=1; | mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> commit; | mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
只有当客户端Acommit提交了事务,其执行结果,客户端B才能看到
但这就造成了
同一个事物内,同样的select,在不同时间查看到的数据不同,这叫做不可重复读
不可重复读也是问题
比如老师根据学生的成绩发放不同奖励,比如60-70是一个等级,71-80是一个等级
我们统计完了60-70,但此时一个同学的成绩出错了,少加了十分,原先是63,改为73,此时数据库select会在71~80读到他的名字,这样他就会出现两个等级中
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
| mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
|
mysql> update account set name='王五' where id=2; |
mysql> insert into account values (3,'赵六',7000); | mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
|
mysql> commit; | mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
|
| mysql> commit;
|
| mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 王五 | 5000.00 |
| | 3 | 赵六 | 7000.00 |
| +----+----------+---------+
在repeatabel read隔离级别下
客户端B的事务无法看到客户端A事务的执行结果,无论是update还是insert,即使客户端A提交也无法查看
这就叫做可重复读
但是,一般的数据库在可重复读状态下,无法屏蔽其他事务insert数据(因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是不会变的,但是新insert的数据也会被读出,导致多次查询时,会多出查找的记录。这种现象叫做幻读
MySQL在repeatable read级别下,解决了幻读的问题(使用了Next-Key锁。即GAP+行锁)
关于锁的介绍可以参看Innodb中的事务隔离级别和锁的关系
对所有操作加锁,进行串行化,不会有问题,但是效率很低,几乎不使用
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
mysql> select * from account; | mysql> select * from account;
+----+----------+---------+ | +----+----------+---------+
| id | name | blance | | | id | name | blance |
+----+----------+---------+ | +----+----------+---------+
| 1 | zhangsan | 4444.00 | | | 1 | zhangsan | 4444.00 |
| 2 | 王五 | 5000.00 | | | 2 | 王五 | 5000.00 |
| 3 | 赵六 | 7000.00 | | | 3 | 赵六 | 7000.00 |
+----+----------+---------+ | +----+----------+---------+
|
mysql> update account set name='张三' where id=1; |
ERROR 1205 (HY000): Lock wait timeout exceeded; try |
restarting transaction |
客户端Aupdate阻塞了,长时间阻塞会超时终止 |
|
mysql> begin; |
mysql> update account set name='张三' where id=1; | mysql> update account set name='张三'
|
Query OK, 1 row affected (23.44 sec) | where id=1;
| ERROR 1213 (40001): Deadlock found c
| when trying to get lock;
| try restarting transaction
|
mysql> begin; | mysql> begin;
update account set name='张三炮' where id=1; |
| mysql> commit;
Query OK, 1 row affected (23.44 sec)
读取不会串行化
,共享锁阻塞
,如果超过一定时间,会超时终止
总结
MySQL默认的隔离级别是可重复读
一致性是由用户决定的
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。