什么是事务
事务是MYSQL的一项功能,它可以使一组数据操作要么全部成功,要么全部失败,不会出现只执行一部分操作的情况
事务的语法结构如下:
1 | sql复制代码start transaction 或者使用 begin 开始事务 |
事务主要有4个特性(ACID):
- A(atomicity):原子性,表示事务中的操作要么全部成功,要么全部失败,像一个整体,不能从中间打断
- C(consistency):一致性,表示数据的完整性不会因为事务的执行而受到破坏
- I(isolation):隔离性,表示多个事务同时执行的过程中,不会相互干扰。不同的隔离级别,相互独立的程度不同
- D(durability):持久性,当事务正确完成后,它对于数据的改变是永久性的
操作原子性
这里我举一个全家的例子,全家的收银员帮顾客结账的情况,这里主要涉及到的就是商品库存表和商品流水表:
stock(库存表)
itemnumer(商品编号) | stock(商品库存) |
---|---|
1 | 10 |
record(流水表)
id(流水单号) | itemnumber(商品编号) | quantity(销售数量) |
---|---|---|
现在,假设门店销售了5个编号为1的商品,这个操作其实意味着我们需要创建一条流水记录,并更新商品库存,一共有两个操作:
- 像record表中插入一条
1号商品卖了5个
的记录 - 把stock表中商品编号为1的记录stock减5
为了避免意外的发生,我们需要将这两个操作放到一个事务中去执行,利用事务的原子性,来确保数据的一致性
这里我省略了建表语句,应该难不倒在座的各位,但要注意itemnumbe
和id
是两张表各自的主键
:satisfied:
1 | sql复制代码mysql> start transaction; |
然后我们可以查询一下结果:
1 | sql复制代码mysql> select * from stock; |
操作的一致性
但是这里有一个细节点需要注意,事务并不会自动帮你处理SQL语句执行中的错误,如果你对事务中某一步的数据操作发生的错误不进行处理,仍然会导致数据不一致
这里如果在insert的过程中发生了错误,但是没有做回滚处理,继续执行后面的操作,最后提交事务,结果就会出现没有流水但是库存减少的情况:
1 | sql复制代码mysql> start transaction; |
然后我们可以查询一下结果:
1 | sql复制代码mysql> select * from stock; |
可以看到并没有往流水表中添加记录,但是库存却被更新了
这就是因为没有正确使用事务导致的数据不完整问题。那么,如何使用事务,才能避免这种由于事务中的某一步或者几步操作出现错误,而导致数据不完整的情况发生呢?这就要用到事务中错误处理和回滚了:
- 如果发现事务中的某个操作发生错误,要及时回滚
- 只有事务中的操作都正常执行,才进行提交
那么我们只需要关注如果判断某个操作发生了错误,我们可以使用MYSQL的函数ROW_COUNT的返回结果,来判断操作是否成功,此时我们把stock表中的1号商品的库存重新调整为5
1 | sql复制代码mysql> update stock set stock = 5 where itemnumber = 1; |
我们可以看到row_count
函数返回的结果是-1
,表示操作执行失败,1
表示操作执行成功,这个时候我们就可以使用rollback
进行回滚
事务的隔离性
这里还是使用全家的例子,只不过我们这次使用的是全家的会员卡,假设索隆的全家会员卡上有50块的余额(也就是5000积分),众所周知,全家的门店会员卡都是互通的,此时索隆拿着自己的实体会员卡来到了全家消费了50元,与此同时,索隆的爱人古伊娜用他的会员卡在全家APP上也消费了50元
索隆在门店消费的时候,开始了一个事务A,包括以下三个操作:
- 读取卡内余额50元
- 更新卡内余额为0
- 插入一条流水记录
古伊娜在APP中付款,开启了一个事务B,也来读取卡内余额,如果B读取卡内余额的操作发生了A更新卡内余额之后,并在插入流水记录之前,那么A有可能由于后面的操作失败而进行回滚。因此,本来可以成功进行的交易,因为读取到错误的信息而导致本可以成功的交易失败了
所以这里我们会用到MYSQL的锁机制。MYSQL可以把A中被修改过且还没有提交的数据锁住,让B处于等待状态,一直到A提交完成或者失败回滚,再释放锁,允许B读取数据,这样可以防止因为A回滚而B读取错误的可能了
MYSQL可以通过使用锁来控制事务对数据的操作,可以实现事务之间的相互隔离,锁的使用方式不同,隔离程度也不同
- READ UNCOMMITED:未提交读,这种就是我们上面举的例子,可以读取事务中还未提交的被更改的数据
- READ COMMITED:已提交读,只能读取事务中已经提交的被更改的数据
- REPEATABLE READ:可重复读,表示在一个事务中,对一个数据读取的值,永远跟第一次读取的值一致,不受其他事务中数据操作的影响。这个也是MYSQL的默认隔离级别
- SERIALIZARBLE:表示任何一个事务,一旦对某一个数据进行了任何操作,那么一直到这个事务结束,MYSQL都会把这个数据锁住。禁止其他事务对这个数据进行任何操作
下面是对各个隔离级别的总结:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
然后我们来依次对各个隔离级别进行讲解
1 | sql复制代码# 可以看到这里默认的隔离级别是可重复读 |
读未提交
- 先打开终端A,然后将MYSQL的默认隔离级别修改为读未提交
- 开启事务但不提交事务
- 然后打开终端B,设置隔离级别,开始事务并修改数据
- 在终端未提交的事务中进行查询
终端A如下:
1 | sql复制代码mysql> set session transaction isolation level read uncommitted; |
终端B如下:
1 | sql复制代码mysql> set session transaction isolation level read uncommitted; |
此时如果终端B中因为某种原因进行了回滚,那么终端A中读取到的就是错误的数据,这也就是所谓的脏读
读已提交(不可重复读)
- 打开一个终端A,并设置当前事务模式为read committed(已提交读),查询表stock的所有记录
- 在终端A的事务提交之前,打开另一个终端B,更新表stock
- 这时,终端B的事务还没提交,终端A不能查询到B已经更新的数据,解决了脏读问题
- 终端B的事务提交
- 终端A再次查询,结果与上一步不一致,产生了不可重复读的问题
终端A如下:
1 | sql复制代码mysql> set session transaction isolation level read committed; |
终端B如下:
1 | sql复制代码mysql> set session transaction isolation level read committed; |
此时由于终端B中进行了事务的提交,终端A中两次读取到的就是不一致的数据,这也就是所谓的不可重复读
可重复读
- 打开一个终端A,并设置当前事务模式为repeatable read,查询表stock的所有记录
- 在终端A的事务提交之前,打开另一个终端B,更新表stock并提交
- 在终端A查询表stock的所有记录,与步骤1查询结果一致,没有出现不可重复读的问题
- 在终端A,接着执行更新操作,数据的一致性没有被破坏
- 重新打开终端B,插入一条新数据后提交
- 接着在终端A中也插入步骤5同样的一条数据,发现无法插入,产生了
幻读
终端A:
1 | sql复制代码mysql> set session transaction isolation level repeatable read; |
终端B:
1 | sql复制代码mysql> set session transaction isolation level repeatable read; |
串行化
- 打开一个终端A,并设置当前事务模式为serializable,查询表stock的所有记录
- 打开终端B,开始事务,并且往stock表中添加一条记录,但是不提交事务
- 在终端A中再次查询stock表,发现无法查询
- 在终端B中提交事务
- 再次在终端A中查询
终端A:
1 | sql复制代码mysql> set session transaction isolation level serializable; |
终端B:
1 | sql复制代码mysql> set session transaction isolation level serializable; |
可见串行化的数据库并发能力是最弱的,所以除非是要数据强一致,否则应该使用MYSQL数据库默认的可重复读的隔离级别
本文转载自: 掘金