我们在银行转账的操作过程中,表面上看是一个单纯的转账操作,但是背后却需要一条或多条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数据库连接对象,以避免对计算机内存造成过多的占用和浪费。