栏目:数据库
1604
唐小碎发布于:2021-12-06 21:24:26
标签:
Mysql
注意,本文讨论的前提是:数据引擎是InnoDB。myISAM不支持事务,其他第三方数据引擎也不在本文的讨论范围内。
## 一,什么是事务
### 1,概述
在mysql中,我们一些业务常常需要保证多条sql语句要么都执行成功,要么都执行不成功。最常见的例子就是:银行转账。
我们假设银行现在只有两个客户,A,B。A同学卡里有100块钱,B同学卡里也有100块钱。现在,A同学要给B同学转账十块钱。这一个转账的动作,就是一个事务,现在这个事务里面需要有两条sql语句:
1. A同学的卡需要扣除十块钱: 100 - 10 = 90
2. B同学的卡需要增加十块钱: 100 + 10 = 110
mysql要通过事务来保证,在这一个事务里面,所有的sql语句,要么都执行成功(转账成功),要么都执行不成功(转账失败)。这样才能保证银行里面总的钱(200块),没有凭空变多,也没有凭空变少。要么是:100+100,要么是110+90。
设想一下,如果没有事务,会可能发生什么情况?在上面转账的这个事务中,如果刚刚在执行完第一条sql的时候,数据库死机了,导致第二条sql没有执行,重新开机后,会发生什么情况?此时,A同学的卡里只有:90块,B同学的卡里只有100块。就是银行里面的总的钱,少了(90 + 100 = 190)。我们不希望有这种情况发生,所以就有了事务。
在一个mysql事务中,里面所有的sql语句,要么都执行成功,要么都不成功,只要有一条sql执行不成功,其他的sql都会执行回滚。不管是服务器宕机,还是sql语句错误。
### 2,使用事务
假设上面例子的数据表(pocket)是下面这样的`select * from pocket;`
| id | user_id | user_name | money |
| :--: | :-----: | :-------: | ----- |
| 1 | 1 | `A同学` | 100 |
| 2 | 2 | `B同学` | 100 |
上面例子的事务,我们可以这样写:
````shell
mysql> use thinkadmin5
Database changed
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update pocket set money = (100-10) where user_id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update pocket set money = (100+10) where user_id = 2;
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)
````
我们通过navicat或者phpMyAdmin等mysql可视化工具,可以注意到,直至我们敲出commit(提交事务)前,数据库里面两位同学的钱都是100块的。最后我们commit之后,数据表才变成下面:
| id | user_id | user_name | money |
| :--: | :-----: | :-------: | ----- |
| 1 | 1 | `A同学` | 90 |
| 2 | 2 | `B同学` | 110 |
如果我们编写的程序在执行一个事务期间,我们的程序发生了异常,我们可以使用rollback(事务回滚),来恢复该事务里所有的sql改动。
````
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update pocket set money = (100-10) where user_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update pocket set money = (100+10) where user_id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from pocket;
+----+---------+-----------+-------+
| id | user_id | user_name | money |
+----+---------+-----------+-------+
| 1 | 1 | A同学 | 100 |
| 2 | 2 | B同学 | 100 |
+----+---------+-----------+-------+
2 rows in set (0.00 sec)
````
可以看到事务回滚后,数据都没有发生改变。
## 二,事务的特征(ACID):
事务有4个特征,或者说,事务必须要满足以下4个条件
### 1,原子性(Atomicity)
原子性描述事务是一个不可再分的最小工作单位,这个最小工作单位里面无论有多少条sql语句,整个事务要么全部提交成功,要么全部失败会滚。不可能只执行其中的一部分sql。
个人理解:一个事务,就好比显示生活中的一颗原子。任何化学反应,都不可能改变这颗原子的性质,它是最小的,不可再分的。
### 2,一致性(Consistency)
一致性描述一个事务,从发生前到发生后,从一个状态改变到另外一个状态。但在整个数据库来看,数据是一致的。从上面描述的转账例子来分析,我们用上帝视角来看 上面的事务(A给B转账10元),银行总的钱,在事务发生前后都是一致的,都是200块。即使数据库奔溃,也是一致的。
### 3,隔离性(Isolation)
隔离性描述两个事务之间,通常是互不干扰的,他们之间是隔离的。事务在提交前做的数据改动,通常对其他事务是不可见的。为什么此处要加上 “通常”,因为当隔离级别为:未提交读(read uncommitted)的时候,X事务是可以读到Y事务未提交的数据更改的,而我们通常不会把隔离级别设置为未提交读。关于隔离级别下面也会讨论到。
Oh!!! Mysql又是如何做到的呢?你别管,它就是可以做到。如果你硬要管,可以搜索关键词:锁(lock)和多版本并发控制(MVCC)。
### 4,持久性(Durability)
持久性描述事务提交(commit)之后,发生的改变是永久的,提交之后就算是数据库死机重启之,数据也是提交后的数据。
## 三,mysql如何保证事务的原子性
原子性是基于日志的Redo/Undo机制,它们将所有对数据的更新操作都写到日志中
Redo log用来记录某数据块被修改后的值,可以用来恢复未写入 data file 的已成功事务更新的数据,MySQL事务在工作时,当commit发生后第一时间是写入到online 的redo日志中,最后才写入到数据库进行持久化,如果这个时候commit成功了,但是服务器死机了,在服务器重启的时候就可以读取redo log来重新把日志中的数据持久化到数据库中去;
Undo log是用来记录数据更新前的值,保证数据更新失败能够回滚,例如上面例子中,A同学在转出的时候扣去10块 之前是100块,这个时候数据库update了变成了90,恰恰这个时候数据库挂了,在重启服务进行 crash-recovery 时就会读取undo log来回滚之前的更新操作,使数据恢复为原来的数据。
## 四,mysql如何保证事务的一致性
一方面,数据的一致性是由事务的原子性来保证的,如果mysql保证了事务的原子性,那么,一个事务下来,数据的一致性就能得到保证。
另一方面,数据的一致性,业务逻辑的一致性,也要由我们程序员自己去保证。比如上面的转账例子,我们每一条sql update语句都返回更新成功(Query OK, 1 row affected),那么我们执行commit(提交事务),否则,我们执行rollback(事务回滚)。
**ACID里的AID都是数据库的特征,也就是依赖数据库的具体实现.而唯独这个C,实际上它依赖于应用层,也就是依赖于开发者.这里的一致性是指系统从一个正确的状态,迁移到另一个正确的状态.什么叫正确的状态呢?就是当前的状态满足预定的约束就叫做正确的状态.而事务具备ACID里C的特性是说通过事务的AID来保证我们的一致性.**
做个比喻事务就好比一个保镖,我们提到事务就会说ACID,而我们提到保镖会说强壮,保护安全,好功夫,踏实.这里强壮,好功夫和踏实都是保镖自己的特征,而安全是属于你的,而你通过保镖的特征来保护你的安全.
关于事务的原子性和一致性,我们可以参考这篇博文:https://blog.csdn.net/weixin_43944305/article/details/109445755
## 五,mysql的隔离级别
mysql支持的隔离级别:从低到高,分别是:read uncommitted(未提交读),read committed(提交读,大部分数据库默认,如oracle),repeatable read(可重复读,mysql默认),serializable(可串行化)。
越低级的隔离级别,系统的开销越少,可以承受更高的并发。越高的隔离级别,事务之间的隔离效果越好。
### 1,设置隔离级别
设置本次mysql实例的隔离级别可以使用:
```
mysql> show global variables like '%isolation%';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
| tx_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
2 rows in set (0.02 sec)
mysql> set global transaction_isolation ='read-uncommitted'; <------ 设置隔离级别
Query OK, 0 rows affected (0.01 sec)
mysql> show global variables like '%isolation%';
+-----------------------+------------------+
| Variable_name | Value |
+-----------------------+------------------+
| transaction_isolation | READ-UNCOMMITTED |
| tx_isolation | READ-UNCOMMITTED |
+-----------------------+------------------+
2 rows in set (0.01 sec)
```
注意,上述方法修改隔离级别,会在mysql重启后失效,如果想永久修改mysql的隔离级别,可以修改mysql配置文件my.cnf ,我的my.cnf文件在:/usr/local/etc/my.cnf。添加或修改下面配置:
````
#设置事务隔离级别:
transaction_isolation = REPEATABLE-READ
````
然后重启,登录mysql客户端,通过`show global variables like '%isolation%';`即可查看隔离级别被更改了,此法更改为永久性更改。
### 2,各隔离级别的隔离效果
| 隔离级别 | 脏读可能性 | 不可重复读可能性 | 幻读可能性 | 加锁读 |
| ---------------------------- | ---------- | ---------------- | ---------- | ------ |
| read uncommitted(未提交读) | Yes | Yes | Yes | No |
| read committed(提交读) | No | Yes | Yes | No |
| repeatable read(可重复读) | No | No | Yes | No |
| serializable(可串行化) | No | No | No | Yes |
上表来自《高性能mysql》第三版第9页。
##### 1,read uncommitted(未提交读)
未提交读这一隔离级别,X事务对数据的更改,X事务还没有提交时,Y事务可以看到X事务更改后的数据。所以叫做未提交读。该隔离级别下,会发生脏读(Dirty Read),这会导致很多奇怪的问题发生。
脏读(Dirty Read):还是用上面讨论的例子,我们的A同学账上有100块。此时A同学打算往B同学转帐100元(X事务),那么第一条sql :
````
update pocket set money = (100-100) where user_id = 1;
````
执行成功后,恰巧另一个银行卡短信通知功能扣费程序(Y事务)启动了,Y事务查询A同学的账上余额,发现是0元,然后,它就会做一些逻辑,比如发送短息提醒充值,甚至断掉这个短信通知服务。
然后如果此时X事务回滚了,A同学的账上还有100块,那么扣费程序所做的所有事情,都是错误的。在更严重的情况下,会发生很严重的程序漏洞,所以实际应用中,几乎不会设置隔离级别为未提交读。
**注意:未提交读会发生脏读,但是不会发生脏写,因为X事务只update A同学余额但没有commit的时候,此时如果Y事务想对A同学的余额做更改,Y事务会一直阻塞。直至X事务commit或者rollback ,Y事务才能写入。这是mysql的锁机制实现的,X事务的update 语句为pocket表id=1这一行(行级锁),加了读锁,Y事务只能读,不能写。**
##### 2,read committed(提交读)
提交读解决了脏读的问题,X事务没提交的数据,Y事务无法读取到。大多数数据库默认采用该隔离级别,如oracle,该隔离级别下,事务只能读到其他事务已经commit到数据。但是这个隔离级别会发生:不可重复读,即两次读数据时,得到的结果不一样。例如下面这种情况:
1. A同学的账户余额是100元
2. X事务修改A同学的账户余额为90元,但此时X事务还没提交
3. Y事务读取A同学的账户余额,得到100元,因为X事务还没提交嘛
4. X事务提交事务,X事务对A同学的余额的更改持久化了
5. Y处理完自己的逻辑,再次读A同学读余额,得到了90元。发现和之前读的不一样了。
所以,提交读还有另外一个名称,叫做:不可重复读(nonrepeatable read)。
##### 3,repeatable read(可重复读)
这是mysql的默认隔离级别,在该隔离级别下,上面👆的情况就不会发生了。该级别保证了一个事务内,无论读多少次,得到的数据都是一样的。
但是可重复读隔离级别下,还是有一个问题,就是幻读(Phantom Read)。所谓幻读,指的是当Y事务在读取某个范围内的记录时,X事务又在范围内插入新的记录,当Y再次读取该范围时,会产生幻行(Phantom Row)。
InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题(这句话是《高性能mysql》第8页的原话)。但是我在自己的电脑上面(mysql5.7)测试发现,确实还有幻行的情况出现,这是什么原因呢?我还没有搞清楚。先标记一下,参考博文:
1. [MVCC能否解决幻读?](https://blog.csdn.net/qq_35590091/article/details/107734005)
2. [MVCC 能解决幻读吗?](https://blog.csdn.net/qq_35590091/article/details/107734005)
3. [既然MySQL中InnoDB使用MVCC,为什么REPEATABLE-READ不能消除幻读?](https://www.zhihu.com/question/334408495)
4. [MySQL中的MVCC到底能不能解决幻读](https://juejin.cn/post/6916500638457298958)
##### 4,serializable(可串行化)
该隔离级是mysql最高的隔离级别,该隔离级别强制事务串行执行,避免了幻读的问题。简单来说,可串行化在读取的每一行数据都加上锁,所以可能导致大量的超时和锁竞争。实际应用中很少用到这个隔离级别,只有非常需要确保数据的一致性的情况下才考虑用。
至此,mysql的事务已经介绍完啦,如果写的有误,欢迎各位大神指正或讨论。
评论
蜜蜜 2021-12-07 09:04:18
又更新啦 加油