MySQL事务的基础知识

image-20240812170509767

一、数据库事务概述

1.1、存储引擎支持事务的情况

show engines;

image-20240812170840053

我们从图中可以看出来目前只有InnoDB支持事务!

1.2、事务的基本概念

数据库事务是指一系列数据库操作的逻辑单元,这些操作要么全部成功执行,要么全部回滚。它的目的是确保数据的一致性和完整性。

⽐如,我们去银⾏转账,操作可以分为下⾯两个环节:

  • 从第⼀个账户划出款项。
  • 将款项存⼊第⼆个账户。

在这个过程中,两个环节是关联的。第⼀个账户划出款项必须保证正确的存⼊第⼆个账户,如果第⼆个 环节没有完成,整个的过程都应该取消,否则就会发⽣丢失款项的问题。整个交易过程,可以看作是⼀ 个事务,成功则全部成功,失败则需要全部撤消,这样可以避免当操作的中间环节出现问题时,产⽣数 据不⼀致的问题。

**事务:**一组逻辑操作单元,使数据从一种状态变换到另一种状态。

**事务处理的原则:**保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交( commit ),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚( rollback )到最初状态。

1.3、事务的ACID

事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

原子性(atomicity)

一致性(consistency)

隔离性(isolation)

持久性(durability)

1.3.1、原子性(atomicity)

一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不能只执行其中的一部分操作。比如:

连老师借给李老师1000元:

1.连老师工资卡扣除1000元

2.李老师工资卡增加1000元

整个事务的操作要么全部成功,要么全部失败,不能出现连老师工资卡扣除,但是李老师工资卡不增加的情况。如果原子性不能保证,就会很自然的出现一致性问题。

1.3.2、一致性(consistency)

一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏。

连老师借给李老师1000元:

1.连老师工资卡扣除1000元

2.李老师工资卡增加1000元

扣除的钱(-500) 与增加的钱(500) 相加应该为0,或者说连老师和李老师的账户的钱加起来,前后应该不变。

1.3.3、持久性(durability)

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

持久性是通过事务日志来保证的。日志包括了重做日志和回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

1.3.4.隔离性(isolation)

一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

如果隔离性不能保证,会导致什么问题?

连老师借给李老师生活费,借了两次,每次都是1000,连老师的卡里开始有10000,李老师的卡里开始有500,从理论上,借完后,连老师的卡里有8000,李老师的卡里应该有2500。

我们将连老师向李老师同时进行的两次转账操作分别称为T1和T2,在现实世界中T1和T2是应该没有关系的,可以先执行完T1,再执行T2,或者先执行完T2,再执行T1,结果都是一样的。但是很不幸,真实的数据库中T1和T2的操作可能交替执行的,执行顺序就有可能是:

image-20240812181648802

如果按照上图中的执行顺序来进行两次转账的话,最终我们看到,连老师的账户里还剩9000元钱,相当于只扣了1000元钱,但是李老师的账户里却成了2500元钱,多了10000元,这银行岂不是要亏死了?

所以对于现实世界中状态转换对应的某些数据库操作来说,不仅要保证这些操作以原子性的方式执行完成,而且要保证其它的状态转换不会影响到本次状态转换,这个规则被称之为隔离性。

二、如何使用事务

使用事务有两种方式,分别为显式事务和隐式事务。

2.1、显示事务

步骤1: START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务。

BEGIN;
START TRANSACTION;

START TRANSACTION 语句相较于BEGIN 特别之处在于,后边能跟随几个修饰符:

① READ ONLY :标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。

② READ WRITE :标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。

③ WITH CONSISTENT SNAPSHOT :启动一致性读。

**步骤2:**一系列事务中的操作(主要是DML,不含DDL)

**步骤3:**提交事务 或 中止事务(即回滚事务)

# 提交事务。当提交事务后,对数据库的修改是永久性的。
COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改
ROLLBACK;
# 将事务回滚到某个保存点。
ROLLBACK TO [SAVEPOINT]

情景演示环境准备:

假设我们有一个电子商务应用,其中有 usersproductsorders 三张表。

CREATE DATABASE ecommerce;

USE ecommerce;

CREATE TABLE users
(
    id       INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50)    NOT NULL,
    email    VARCHAR(100)   NOT NULL,
    balance  DECIMAL(10, 2) NOT NULL DEFAULT 0.00
);

CREATE TABLE products
(
    id    INT AUTO_INCREMENT PRIMARY KEY,
    name  VARCHAR(100)   NOT NULL,
    price DECIMAL(10, 2) NOT NULL
);

CREATE TABLE orders
(
    id          INT AUTO_INCREMENT PRIMARY KEY,
    user_id     INT,
    product_id  INT,
    quantity    INT            NOT NULL,
    total_price DECIMAL(10, 2) NOT NULL,
    order_date  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users (id),
    FOREIGN KEY (product_id) REFERENCES products (id)
);
INSERT INTO users (username, email, balance)
VALUES ('Alice', 'alice@example.com', 100.00),
       ('Bob', 'bob@example.com', 50.00),
       ('Charlie', 'charlie@example.com', 75.00);

INSERT INTO products (name, price)
VALUES ('Laptop', 999.99),
       ('Phone', 499.99),
       ('Headphones', 149.99);
       
INSERT INTO users (username, email, balance)
VALUES ('Alice', 'alice@example.com', 100.00),
       ('Bob', 'bob@example.com', 50.00),
       ('Charlie', 'charlie@example.com', 75.00);

INSERT INTO products (name, price)
VALUES ('Laptop', 999.99),
       ('Phone', 499.99),
       ('Headphones', 149.99);       

2.1.1、情景演示1

假设用户想购买一个产品,我们需要从用户的账户余额中扣除相应的金额,并在 orders 表中记录订单。如果任何一步失败,我们需要回滚事务。

开启事务:

START TRANSACTION;

执行事务中的操作

  1. 检查用户的余额是否足够支付产品。假设用户ID为1的用户尝试购买产品

    SELECT balance FROM users WHERE id = 1; 
    
  2. 扣除用户的余额。

    UPDATE users SET balance = balance - 499.99 WHERE id = 1;
    
  3. 插入订单记录。

    INSERT INTO orders (user_id, product_id, quantity, total_price) VALUES (1, 2, 1, 499.99);
    
  4. 我们再次查看相应的余额

    SELECT balance FROM users WHERE id = 1; 
    

提交事务
如果所有操作成功执行,则提交事务:

COMMIT;

回滚事务
如果某一步失败(例如用户的余额不足),则回滚事务:

ROLLBACK;

image-20240812190643458

SELECT balance FROM users WHERE id = 1; 

image-20240812190705285

2.1.2、情景演示2

如果你开启了一个事务,执行了很多语句,忽然发现某条语句有点问题,你只好使用ROLLBACK语句来让数据库状态恢复到事务执行之前的样子,然后一切从头再来,但是可能根据业务和数据的变化,不需要全部回滚。所以MySQL里提出了一个保存点(英文:savepoint)的概念,就是在事务对应的数据库语句中打几个点,我们在调用ROLLBACK语句时可以指定会滚到哪个点,而不是回到最初的原点。定义保存点的语法如下:

SAVEPOINT 保存点名称;

当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词WORK和SAVEPOINT是可有可无的):

ROLLBACK TO [SAVEPOINT] 保存点名称;

不过如果ROLLBACK语句后边不跟随保存点名称的话,会直接回滚到事务执行之前的状态。

如果我们想删除某个保存点,可以使用这个语句:

RELEASE SAVEPOINT 保存点名称;

我们仍然在之前的 ecommerce 数据库中操作,且用户Alice(用户ID为1)想要购买多个产品。如果某个产品的购买失败,我们希望回滚到上一个成功的操作,而不是回滚整个事务。

步骤1:开启事务并设置保存点

  1. 首先,启动事务:

    START TRANSACTION;
    
  2. 检查Alice的余额:

    SELECT balance FROM users WHERE id = 1;
    
    • 结果:100.00(Alice的余额为100.00)
  3. 设置第一个保存点:

    SAVEPOINT before_first_purchase;
    

步骤2:执行第一个操作并回滚到保存点

  1. Alice尝试购买一副价格为149.99的耳机(Headphones,产品ID为3):

    UPDATE users SET balance = balance - 149.99 WHERE id = 1;
    INSERT INTO orders (user_id, product_id, quantity, total_price) VALUES (1, 3, 1, 149.99);
    
    SELECT balance FROM users WHERE id = 1;
    
    • 此时,如果执行成功,Alice的余额将变为 -49.99,但余额不足,我们决定回滚到之前的保存点 before_first_purchase
  2. 回滚到保存点:

    ROLLBACK TO before_first_purchase;
    
    • 现在,Alice的余额恢复到之前的状态,即 100.00,并且这个购买操作被撤销。
    SELECT balance FROM users WHERE id = 1;
    

image-20240812191751311

image-20240812191805847

步骤3:继续进行其他操作并提交事务

  1. 现在Alice决定购买一个价格较低的产品,例如手机(Phone,产品ID为2,价格为499.99)。我们继续设置新的保存点并执行操作:

    SAVEPOINT before_second_purchase;
    

    由于她的余额依然不足,这次操作也会失败,我们可以再次选择回滚到保存点:

    ROLLBACK TO before_second_purchase;
    
  2. 最后,Alice决定购买更便宜的产品(假设有一款新产品,产品ID为4,价格为49.99):

    INSERT INTO products (name, price) VALUES ('Charger', 49.99);
    
    UPDATE users SET balance = balance - 49.99 WHERE id = 1;
    
    INSERT INTO orders (user_id, product_id, quantity, total_price) VALUES (1, 4, 1, 49.99);
    
    • 此时,操作成功,Alice的余额为 50.01
    SELECT balance FROM users WHERE id = 1;
    

image-20240812192056045

  1. 提交事务:
COMMIT;
SELECT balance FROM users WHERE id = 1;

image-20240812192203312

2.2、隐式事务

MySQL中有一个系统变量autocommit :

SHOW VARIABLES LIKE 'autocommit';

image-20240812192703751

当然,如果我们想关闭这种自动提交的功能,可以使用下边两种方法之一:

  • 显式的的使用START TRANSACTION 或者BEGIN 语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。

  • 把系统变量autocommit 的值设置为OFF ,就像这样:

    SET autocommit = OFF;
    
    SET autocommit = 0;
    

当我们使用START TRANSACTION或者BEGIN语句开启了一个事务,或者把系统变量autocommit的值设置为OFF时,事务就不会进行自动提交,但是如果我们输入了某些语句之后就会悄悄的提交掉,就像我们输入了COMMIT语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交,这些会导致事务隐式提交的语句包括:

2.2.1、执行DDL

定义或修改数据库对象的数据定义语言(Datadefinition language,缩写为:DDL)。

所谓的数据库对象,指的就是数据库、表、视图、存储过程等等这些东西。当我们使用CREATE、ALTER、DROP等语句去修改这些所谓的数据库对象时,就会隐式的提交前边语句所属于的事务。

2.2.2、隐式使用或修改mysql数据库中的表

当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。

2.2.3、事务控制或关于锁定的语句

当我们在一个会话里,一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务

2.2.4、加载数据的语句

比如我们使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。

2.2.5、关于MySQL复制的一些语句

使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时也会隐式的提交前边语句所属的事务。

2.2.6、其它的一些语句

使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。

三、事务隔离级别

MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session )。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。事务有隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对性能影响太大,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,那就看二者如何权衡取舍了。

概述:

事务隔离级别的问题主要来源于在多用户环境下如何权衡数据一致性与系统性能之间的矛盾。在一个数据库系统中,事务隔离性指的是事务在并发执行时不被其他事务的操作所干扰,从而保证数据的一致性和正确性。然而,严格的隔离性会导致系统的性能下降,因为这通常意味着需要更频繁地锁定数据和等待。

3.1、事务并发引发的问题

在上面我们说过事务有一个称之为隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据,这样的话并发事务的执行就变成了串行化执行。

但是对串行化执行性能影响太大,我们既想保持事务的一定的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,当我们舍弃隔离性的时候,可能会带来什么样的数据问题呢?

问题演示的数据库准备:

DROP TABLE IF EXISTS `accounts`;
CREATE TABLE `accounts`  (
  `account_id` int NOT NULL,
  `account_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `balance` decimal(10, 2) NULL DEFAULT NULL,
  PRIMARY KEY (`account_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

INSERT INTO `accounts` VALUES (1, 'A', 500.00);
INSERT INTO `accounts` VALUES (2, 'B', 500.00);
INSERT INTO `accounts` VALUES (3, 'C', 500.00);

SET FOREIGN_KEY_CHECKS = 1;

为了后续的演示清楚,我们统一会话的隔离界别:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

image-20240812201118056

image-20240812201126964

3.1.1、脏读

当一个事务读取到了另外一个事务修改但未提交的数据,被称为脏读。

image-20240812195828151

1、在事务A执⾏过程中,事务A对数据资源进⾏了修改,事务B读取了事务A修改后的数据。
2、由于某些原因,事务A并没有完成提交,发⽣了RollBack操作,则事务B读取的数据就是脏数据。
这种读取到另⼀个事务未提交的数据的现象就是脏读(Dirty Read)。

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 此时,A账户的余额变成了400,但还没有提交
select * from accounts WHERE account_id = 1;

image-20240812201615301

START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1;
-- 读取到A账户的余额是400(脏读,因为Session 1还没有提交事务)

image-20240812201634588

ROLLBACK;
-- 回滚事务,A账户的余额恢复到500
SELECT balance FROM accounts WHERE account_id = 1;

image-20240812201913312

ROLLBACK;
SELECT balance FROM accounts WHERE account_id = 1;

image-20240812201947408

3.1.2、不可重复读

当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象称为不可重复读。

image-20240812202121368

事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的数据不⼀致。

START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1;
-- 读取到A账户的余额是500

image-20240812202402775

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
COMMIT;
-- 提交事务,A账户的余额变成400
SELECT balance FROM accounts WHERE account_id = 1;

image-20240812202617878

SELECT balance FROM accounts WHERE account_id = 1;

image-20240812202714617

COMMIT;

image-20240812202832518

3.1.3、幻读

在事务执行过程中,另一个事务将新记录添加到正在读取的事务中时,会发生幻读。

image-20240812202908479

事务B前后两次读取同⼀个范围的数据,在事务B两次读取的过程中事务A新增了数据,导致事务B后⼀
次读取到前⼀次查询没有看到的⾏。
幻读和不可重复读有些类似,但是幻读重点强调了读取到了之前读取没有获取到的记录。

START TRANSACTION;
SELECT * FROM accounts WHERE balance = 500;
-- 读取到两条记录,账户B和C

image-20240812203247355

START TRANSACTION;
INSERT INTO accounts (account_id, account_name, balance) VALUES (4, 'D', 500);
-- 提交事务,新增了一个余额为500的账户D
COMMIT;
SELECT * FROM accounts WHERE balance = 500;

image-20240812203359247

SELECT * FROM accounts WHERE balance = 500;

image-20240812203430259

COMMIT;

image-20240812203453535

3.2、SQL标准中的四种隔离级别

我们上边介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题也有轻重缓急之分,我们给这些问题按照严重性来排一下序:

脏读 > 不可重复读 > 幻读

我们上边所说的舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,越严重的问题就越可能发生。

SQL-92 标准还定义了 4 种隔离级别来解决脏读、幻读、不可重复读等这些异常情况,从高到底依次为:可串行化(Serializable)、可重复读(Repeatable reads)、提交读(Read committed)、未提交读(Read uncommitted)。

SQL标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:

READ UNCOMMITTED隔离级别下,可能发生脏读、不可重复读和幻读问题。

READ COMMITTED隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题。

REPEATABLE READ隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题。

SERIALIZABLE隔离级别下,各种问题都不可以发生。

image-20240812204349922

不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4种事务隔离级别与并发性能的关系如下:

image-20240812205030284

3.3、设置MySQL中的隔离级别

查看隔离界别

SHOW VARIABLES LIKE 'transaction_isolation';

image-20240812205711043

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
#其中,隔离级别格式:
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

或者

SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
#其中,隔离级别格式:
READ-UNCOMMITTED
READ-COMMITTED
REPEATABLE-READ
SERIALIZABLE

关于设置时使用GLOBAL或SESSION的影响:

使用GLOBAL 关键字(在全局范围影响):

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

或者:

SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';

则:

  • 当前已经存在的会话无效
  • 只对执行完该语句之后产生的会话起作用

使用SESSION 关键字(在会话范围影响):

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

或者:

SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';

则:

  • 对当前会话的所有后续的事务有效
  • 如果在事务之间执行,则对后续的事务有效
  • 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务

小结:
数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值