MySQL数据库之事务(第3节)


我们在银行转账的操作过程中,表面上看是一个单纯的转账操作,但是背后却需要一条或多条SQL语句来完成转账操作,这一组SQL语句是一个整体,被称为事务

假设我们现在需要操作数据库进行转账,A账户给B账户转账100元,我们就需要操作数据库,先扣除A账户100元,再给B账户添加100元,这样才实现转账成功。但如果在扣除A账户100元后,服务器突然出现中断,A账户钱少了100元,但B账户又没有收到钱,此时就需要MySQL事务来处理了。

1、MySQL事务的定义

MySQL事务指的是由一个或者多个数据库操作单元组成,相当于把多个sql语句打包成一个整体,这个整体就叫做事务,执行的时候,它们必须要么全部成功执行,要么全部不执行。MySQL事务被设计为确保数据库中的数据的完整性和一致性,即使在并发访问的情况下也是如此。

在大型的程序中,一个MySQL数据库可不止一个事务在运行,同一时刻,有大量的请求被包装成事务,向MySQL服务器发起事务处理请求。在这种并发访问的情况下,事务确保数据的正确性,而不会出现数据不一致或错误的情况。

那么,事务是怎么做到“要么全部成功,要么全部失败”的呢?其原理比较简单,当事务执行失败时,也就是在执行事务内部的sql语句时,数据库发现错误,那它就会自动还原成事务之前的状态,撤销之前的sql,这样的操作叫做 “回滚”(rollback)。

为了实现回滚,数据库管理系统会记录事务的所有操作到日志中。如果需要回滚,系统会根据日志中的记录撤销操作,将数据库状态恢复到事务开始之前的状态。

2、MySQL事务的四个特性

MySQL事务的四个特性通常被称为ACID属性,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),它们共同确保了事务的可靠性和数据的一致性。

原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。如果事务中的任何操作失败,整个事务将被回滚,将数据库状态恢复到事务开始之前的状态(最重要的特性)。

一致性(Consistency):在事务开始之前和事务结束之后,数据库都必须处于一致的状态,即数据库的完整性约束没有被破坏。

隔离性(Isolation):并发执行的事务之间不会互相影响。每个事务都应该像在独立运行一样,即使多个事务同时进行,它们也应该彼此隔离,以避免数据不一致的问题。

持久性(Durability):一旦事务被提交,它对数据库的改变就是永久性的。即使系统发生故障,已经提交的事务的结果也不会丢失。

3、Python中的MySQL事务

在Python数据库编程中,当游标对象建立之时,就自动开始了一个隐形的数据库事务。我们可以通过手动调用commit()方法和rollback()方法来管理事务,commit()方法用于提交所有更新操作,rollback()方法用于回滚当前游标的所有操作。每一个方法都开始了一个新的事务。

下面我们以用户转账为例,使用pymysql模块进行MySQL事务管理,首先我们在“pyhint”数据库中创建一张名为“account”的表,并定义三个列:id、name和money。再通过“insert into”语句将3条数据插入到表中,例如:

动手练一练:

# 导入pymysql模块
import pymysql

# 创建connect对象,建立数据库连接
db = pymysql.connect(host="localhost", user="root", password="123456", database="pyhint")

# 创建游标对象
cursor = db.cursor()

# 使用execute()方法执行SQL语句,创建一张“account”表
sql = """
create table account (
    id int(6) unsigned auto_increment primary key,
    name varchar(30) not null,
    money bigint not null
);
"""
cursor.execute(sql)

# 使用预处理语句插入数据
sql = """
insert into account (name, money)
values ('许某', 5000), ('陈某', 5000), ('林某', 5000);
"""

try:
    # 执行sql语句,并返回受影响的行数
    row_count = cursor.execute(sql)
    # 提交到数据库执行
    db.commit()
    # 打印结果
    if row_count > 0:
        print("数据插入成功!")
    else:
        print("数据插入失败!")
except Exception as e: 
    # 如果发生错误则回滚
    db.rollback()

# 关闭游标,释放资源
cursor.close()

# 关闭数据库连接
db.close()

执行以上代码,输出结果为:

数据插入成功

接下来,我们开始模拟用户转账,即许某转账1000元给陈某,例如:

动手练一练:

# 导入pymysql模块
import pymysql

# 创建connect对象,建立数据库连接
db = pymysql.connect(host="localhost", user="root", password="123456", database="pyhint")

# 创建游标对象
cursor = db.cursor()

# 查询所有数据,返回数据为元组格式
cursor.execute("select * from account;")

# 使用fetchall()方法获取所有的查询结果
result = cursor.fetchall()
print("转账前的数据:", result)

try:
    # 开启事务, 标记着: 转账从这里开始.
    db.begin()
    # 执行SQL语句, 获取结果集.
    # 许某扣除1000元
    sql1 = 'update account set money = money - 1000 where name="许某";'
    row_count1 = cursor.execute(sql1)

    # 陈某添加1000元
    sql2 = 'update account set money = money + 1000 where name="陈某";'
    row_count2 = cursor.execute(sql2)
except Exception as e:
    # 到这里, 说明程序出问题了, 提示, 然后回滚即可.
    db.rollback()     # 事务回滚
    print('程序出问题了, 请稍后再试!')
else:
    # 走到这里, 说明try没有问题, 提交事务即可.
    db.commit()       # 提交事务
    print('转账成功' if row_count1 == 1 and row_count2 == 1 else '转账失败')
    # 再次查询所有数据,返回数据为元组格式
    cursor.execute("select * from account;")
    # 使用fetchall()方法获取所有的查询结果
    result = cursor.fetchall()
    print("转账后的数据:", result)

finally:
    # 释放资源, 无论try内容成功与否, 都会走这里.
    cursor.close()
    db.close()
    print('释放资源完毕!')

执行以上代码,输出结果为:

转账前的数据 ((1, '许某', 5000), (2, '陈某', 5000), (3, '林某', 5000))
转账成功
转账后的数据 ((1, '许某', 4000), (2, '陈某', 6000), (3, '林某', 5000))
释放资源完毕!

上面的例子中,我们使用pymysql模块通过MySQL事务,执行了两条“更新”的SQL语句,只要有一条错误,两条都不会生效。此过程如果出现异常则使用rollback()回滚方法使数据库恢复到执行SQL语句之前的状态。最后,依次关闭cursor游标对象和connect数据库连接对象,以避免对计算机内存造成过多的占用和浪费。