通过实例理解MySQL事务及几种锁(一)

前言

最近项目里有个特殊的BUG,排查了好半天才发现是因为事务引起的。因代码中采用了更精细的编程式事务,与Service方法中的传统事务属于不同事务,因此得不到想要的数据,最终引起BUG。于是,又回头重新复习了一下MySQL的事务相关知识点,本次的重点将放在对事务的隔离级别的理解上。

准备条件

数据库脚本:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for my
-- ----------------------------
DROP TABLE IF EXISTS `my`;
CREATE TABLE `my`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `money` int(11) NULL DEFAULT NULL,
  `sort` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of my
-- ----------------------------
INSERT INTO `my` VALUES (1, '张三', 50, 1);
INSERT INTO `my` VALUES (2, '李四', 100, 3);
INSERT INTO `my` VALUES (3, '王二', 999, 7);
INSERT INTO `my` VALUES (4, '赵六', 78, 4);
INSERT INTO `my` VALUES (5, '刘七', 200, 5);
INSERT INTO `my` VALUES (6, '孙八', 666, 6);

SET FOREIGN_KEY_CHECKS = 1;
一、基本概念

事务:即一组原子性SQL操作,一个独立的工作单元,或者说是一件事的最小操作单元。事务的语句,要么全部执行成功,要么全部执行失败。

在MySQL中,默认是自动commit的,所以如果要测试事务的话,我们可以将自动提交关闭。也可以用START TRANSACTION语句手动开启一个事务,然后要么使用COMMIT提交事务,要么使用ROLLBACK回滚事务。

例如:

START TRANSACTION;
SELECT name FROM my where id = 3;
UPDATE my SET money = money + 100 where id = 3;
DELETE FROM my where id = 6;
COMMIT;
二、事务的特性(ACID)

从事务的定义来看,事务具有以下四个特性:

原子性(Atomicity)

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性(Consistency)

数据库中的数据总是从一个一致性的状态转换到另外一个一致性的状态。

隔离性(Isolation)

通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。

持久性(Durability)

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。

三、事务的隔离级别

在SQL标准中定义了4中隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低,但是同时也会带来一些问题,较高级别的隔离则相反。

四种隔离级别:

  • Read Uncommitted(未提交读)
  • Read Committed(读已提交)
  • Repeatable Read (可重复读)
  • Serializable(可串行化)

为什么会有着四种隔离级别呢?其实这四种隔离级别是为了解决脏读、幻读、不可重复读等问题。下表展示了几种隔离级别的相关特性。

隔离级别脏读可能性不可重复读可能性幻读可能性加锁读
Read Uncommitted
Read Committed
Repeatable Read
Serializable

**MySQL默认的隔离级别是Repeatable Read,隔离级别可配置。**至于为什么选用这个级别,简单来说是为了解决主从节点之间的复制问题,详细的我们以后再细说。我们今天的重点是来通过几个实例操作来理解几种隔离级别及现象。

我们可以通过以下语句查询数据库的隔离级别和设置隔离级别:

-- 查询当前数据库的隔离级别
SELECT @@tx_isolation;

-- 设置隔离级别(以下语句只针对当前session有效)
-- 格式:set session transaction isolation level 事务隔离级别
set session transaction isolation level read uncommitted;
-- 可选值有read uncommitted、read committed、repeatable read、serializable
Read Uncommitted

在这个级别下,事务中的修改,即使没有提交,对于其他事务来说也是可见的。一个事务可以读取到另外一个事务未提交的数据,这也称之为脏读(Dirty Read)

该级别会导致各种问题,如果事务A读取到了事务B未提交的数据,而事务B在事务A提交后回滚了,那么可能会带来业务上的不正确,会有很多乱子的。

示例:

设置隔离级别

set session transaction isolation level read uncommitted;

当前数据库数据

在这里插入图片描述

SQL操作:

-- 事务A 
START TRANSACTION;
SELECT money FROM my where id = 1;
UPDATE my set money = money + 50 where id = 1;
-- 假设我们此处暂不提交事务,另开一个session去处理事务B
ROLLBACK;
-- 事务B
BEGIN; -- 这是START TRANSACTION的简写
SELECT money FROM my where id = 1;
COMMIT;

假设我们在事务B执行查询id为1的money数据,会发现是100。

在这里插入图片描述

这就是脏读了,如果事务A再这之后回滚了,那么我们在业务中拿到的基础数据再操作就不对了,会导致很多问题的,所以一般如非特别需要,一般不会采用这种隔离级别。

Read Commited

该隔离级别解决了脏读的问题。大多数的数据库系统的默认隔离级别都是Read Committed(MySQL的默认隔离级别是Repeated Read,简称RR),该级别下,一个事务开始时,只能“看见”已经提交的事务所作的修改。换句话说,一个事务从开始到提交结束之前,所做的任何修改对其他事务不可见。该级别有时候也被称为不可重复读(nonrepeatable read),因为两次执行查询,可能会得到不一样的结果。

示例:

设置隔离级别

set session transaction isolation level read committed;

当前数据库数据

在这里插入图片描述

SQL操作同上一节的例子,我们会发现事务B的查询结果就会依然是50;在事务A没有提交之前,事务B无论查询多少次,结果都是50。

在这里插入图片描述

那么对于不可重复读的意思是什么呢?我们通过以下的例子来解释:

-- 事务A 
START TRANSACTION;
SELECT money FROM my where id = 1;
-- 假设此时事务B提交了,我们需要再查询一次id为1的数据
SELECT money FROM my where id = 1;
COMMIT;
-- 事务B
BEGIN;
UPDATE my set money = money + 50 where id = 1;
SELECT money FROM my where id = 1;
COMMIT;

即在事务A执行的过程中,事务B仍具有对id为1的数据的修改或删除的权限,那么在事务B提交前和提交后,事务A两次读取到的同一行的数据就不一样,一次是50,一次就是100,这就是不可重复读的表现。

Repeatable Read

该隔离级别解决了不可重复读的问题,保证了在同一个事务中多次读取同样记录的结果是一致的,正如其名,是可以重复读取的。但是理论上,可重复读隔离级别还是无法解决另外一个问题-幻读(Phantom Read)。所谓幻读,指的是当某个事务A在读取某个范围内的记录时,另外一个事务B又在该范围内插入了新的记录,当之前的事务A若再次读取该范围内的记录时,会得到多几行数据,及所谓的幻行。

示例:

设置隔离级别

set session transaction isolation level repeatable read;

当前数据库数据

在这里插入图片描述

SQL操作:

-- 事务A 
START TRANSACTION;
SELECT * from my where money > 50 and money < 150;
-- 假设此时事务B提交了,我们再查询该范围的数据
SELECT * from my where money > 50 and money < 150;
COMMIT;
-- 事务B
BEGIN;
INSERT INTO my (name, money, sort) VALUES ("九九", 98, 6);
COMMIT;

执行过程结果:

先在事务A中查询money为50-150的人员信息

再执行事务B的Insert操作,并提交后,再次查询50-100的人员信息

不过需要注意的是,这个操作在InnoDB引擎的表中,可能无法复现上述所说的幻读或者幻行的现象,因为InnoDB的存储引擎通过多版本并发控制(MVCC, Multi-Version Concurrency Control)解决了幻行问题。至于是怎么解决的,这里只简单的解释为通过间隙锁控制的。对于间隙锁的概念,我们后面介绍锁的时候再详细看。

Serializable

该隔离级别是最高的隔离级别,它通过强制事务串行执行,避免了前面说的幻读问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题,会造成很大的开销,实际应用中也很少会用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下才会采用该级别。

示例:

设置隔离级别

set session transaction isolation level serializable;
不可重复读、可重复读、幻读的区别

很多人对这三个概念如果理解不到位,可能容易发生混淆。

不可重复读,指的是对相同的数据行多次读取的结果可能不一样,重点是数据修改;

可重复读就是针对不可重复读的概念来的,与其相反,多次读取相同的数据行,结果是一致的;

幻读,则是针对的是新增或者删除的数据,即使是在可重复读的级别下,可能也会在读取范围数据的情况下出现幻行情况。

四、Mysql中的几种锁

《通过实例理解MySQL事务及几种锁(二)》

参考资料
  • 《高性能MySQL》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值