Mysql数据库(十一)事务与锁机制
一、事务机制
1.事务的概念
事务是指一组互相依赖的操作单元的集合,用来保证对数据库的正确修改,保持数据的完整性,如果一个事务的某个单元操作失败,将取消本次事务的全部操作。
比如将A账户的资金转入B账户,在A中扣除成功,在B中添加失败,导致数据失去平衡,事务将回滚到原始状态,即A中没少,B中没多。
事务必须具备以下特征,简称ACID:
原子性(Atomicity):每个事务是一个不可分割的整体,只有所有的操作单元执行成功,整个事务才成功;否则此次事务就失败,所有执行成功的操作单元必须撤销,数据库回到此次事务之前的状态。
一致性(Consistency):在执行一次事务后,关系数据的完整性和业务逻辑的一致性不能被破坏。例如A与B转账结束后,资金总额不能变。
隔离性(Isolation):在并发环境中,一个事务所做的修改必须与其他事务所做的修改相隔离。例如一个事务查看的数据必须是其他并发事务修改之前或修改完毕的数据,不能是修改中的数据。
持久性(Durability):事务结束后,对数据的修改是永久保存的,即使因系统故障导致重启数据库系统,数据依然是修改后的状态。
2.事务机制的必要性
(1)创建银行的数据库和数据表
mysql> CREATE DATABASES db_bank; ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DATABASES db_bank' at line 1 mysql> CREATE DATABASE db_bank; Query OK, 1 row affected (0.00 sec) mysql> use db_bank; Database changed mysql> CREATE TABLE tb_account( -> id int(10) unsigned NOT NULL auto_increment PRIMARY KEY, -> name varchar(30), -> balance FLOAT(8,2) unsigned DEFAULT 0 -> ); Query OK, 0 rows affected (0.02 sec)
mysql> INSERT INTO tb_account(name,balance) values('A',1000),('B',0); Query OK, 2 rows affected (0.01 sec) Records: 2 Duplicates: 0 Warnings: 0 mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 1000.00 | | 2 | B | 0.00 | +----+------+---------+ 2 rows in set (0.00 sec)
(2)创建转账操作存储过程
mysql> delimiter // mysql> CREATE PROCEDURE proc_transfer(IN id_from INT ,IN id_to INT ,IN money INT) -> READS SQL DATA -> BEGIN -> UPDATE tb_account SET balance=balance+money WHERE id=id_to; -> UPDATE tb_account SET balance=balance-money WHERE id=id_from; -> END -> // Query OK, 0 rows affected (0.01 sec)
(3)调用两次存储过程(第一条UPDATE语句没有执行成功,但是第二条UPDATE语句执行成功了,于是就多了500)
mysql> CALL proc_transfer(1,2,700); Query OK, 1 row affected (0.01 sec) mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 700.00 | +----+------+---------+ 2 rows in set (0.00 sec) mysql> CALL proc_transfer(1,2,500); ERROR 1264 (22003): Out of range value for column 'balance' at row 1 mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1200.00 | +----+------+---------+ 2 rows in set (0.00 sec)
3.关闭MySQL自动提交
(1)查看自动提交功能是否关闭(开启时表示,如果不显式地开启一个事务,则每个SQL语句都被当做一个事务执行提交操作)
mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set, 1 warning (0.00 sec)
(2)关闭自动提交功能(所有的SQL语句都是在一个事务中,直到显式地执行提交或者回滚时,该事务才结束,同时又会开启另一个新事务)
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | OFF | +---------------+-------+ 1 row in set, 1 warning (0.00 sec)
4.事务回滚
(1)关闭自动提交功能后转账
mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1200.00 | +----+------+---------+ 2 rows in set (0.00 sec) mysql> call proc_transfer(1,2,500); ERROR 1264 (22003): Out of range value for column 'balance' at row 1 mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1700.00 | +----+------+---------+ 2 rows in set (0.00 sec)
(2)再打开一个命令行,查看数据表(关闭了自动提交功能后,如果不手动提交,那么UPDATE操作的结果将仅仅影响内存中的临时记录,并没有真正写入到数据库文件。)
mysql> use db_bank; Database changed mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1200.00 | +----+------+---------+ 2 rows in set (0.00 sec)
(3)更新后的数据与想要的数据不一致,执行回滚操作
mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1700.00 | +----+------+---------+ 2 rows in set (0.00 sec) mysql> ROLLBACK; Query OK, 0 rows affected (0.00 sec) mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1200.00 | +----+------+---------+ 2 rows in set (0.00 sec)
5.事务提交
(1)显示提交(关闭自动提交功能后,使用COMMIT命令显示地提交更新语句,相当于(ROLLBACK))。
mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1200.00 | +----+------+---------+ 2 rows in set (0.02 sec) mysql> call proc_transfer(1,2,500); ERROR 1264 (22003): Out of range value for column 'balance' at row 1 mysql> COMMIT; Query OK, 0 rows affected (0.00 sec) mysql> select * from tb_account; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1700.00 | +----+------+---------+ 2 rows in set (0.00 sec)
(2)隐式提交(执行“set autocommit=1”除了开启自动提交功能,还会提交之前所有的更新语句)
6.MySQL中的事务(START TRANSACTION语句)
(1)创建存储过程
mysql> DELIMITER // mysql> CREATE PROCEDURE prog_tran_account(IN id_from INT, IN id_to INT, IN money int) -> MODIFIES SQL DATA -> BEGIN -> DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK; -> START TRANSACTION; -> UPDATE tb_account SET balance=balance+money WHERE id=id_to; -> UPDATE tb_account SET balance=balance-money WHERE id=id_from; -> COMMIT; -> END -> // Query OK, 0 rows affected (0.00 sec)
(2)调用存储过程
mysql> CALL prog_tran_account(1,2,700); -> select * from tb_account; -> // Query OK, 0 rows affected (0.00 sec) +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 300.00 | | 2 | B | 1700.00 | +----+------+---------+ 2 rows in set (0.00 sec)
各账户的余额并没有改变,而且也没有出现错误,这是因为对出现的错误进行了处理,并且进行了事务回滚。
mysql> call proc_tran_account(1,2,200); -> select * from tb_account; -> // Query OK, 0 rows affected (0.01 sec) +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 100.00 | | 2 | B | 1900.00 | +----+------+---------+ 2 rows in set (0.01 sec)
将转账金额修改为200元,那么将实现正常转账。
7.回退点
(1)创建存储过程。
mysql> CREATE PROCEDURE proc_savepoint_account() -> MODIFIES SQL DATA -> BEGIN -> DECLARE CONTINUE HANDLER FOR SQLEXCEPTION -> BEGIN -> ROLLBACK TO A; -> COMMIT; -> END; -> START TRANSACTION; -> START TRANSACTION; -> INSERT INTO tb_account(name,balance)VALUES('C', 1000); -> savepoint A; -> UPDATE tb_account SET balance=balance+500 WHERE id=2; -> UPDATE tb_account SET balance=balance-500 WHERE id=1; -> COMMIT; -> END -> // Query OK, 0 rows affected (0.01 sec)
(2)调用存储过程。第一条插入语句执行成功,由于第二条更新语句出现错误,所以事务回滚到A了。
mysql> CALL proc_savepoint_account(); -> select * from tb_account; -> // Query OK, 0 rows affected (0.01 sec) +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 100.00 | | 2 | B | 1900.00 | | 3 | C | 1000.00 | +----+------+---------+ 3 rows in set (0.01 sec)
二、锁机制
数据库管理系统采用锁的机制来管理事务。当多个事务同时修改同一数据时,只允许持有锁的事务修改该数据,其他事务只能“排队等待”,直到前一个事务释放其拥有的锁。
1.MySQL锁机制的基本知识
(1)锁的类型
在处理并发读或写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常称为读锁和写锁。
读锁也称为共享锁,它是共享的,或者说是相互不阻塞的。多个客户端在同一时间可以同时读取同一资源,互补干扰。
写锁也成为排他锁,它是排他的,也就是说一个写锁会阻塞其他的写锁和读锁。这是为了确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源,保证安全。
(2)锁粒度
一种提高共享资源并发性的方式就是让锁定对象更有选择性。也就是尽量只锁定部分数据,而不是所有的资源。这就是颗粒度的概念。它是指锁的作用范围,是为了对数据库中高并发相应和系统性能两方面进行平衡而提出的。
颗粒度越小,并发访问性能越高,越适合做并发更新操作;颗粒度越大,并发访问性能就越低,越适合做并发查询操作。
在给定的资源上,锁定的数据量越少,系统的并发程度越高,完成某个功能时所需要的加锁和解锁的次数就会越多,反而会消耗较多的资源,甚至会出现资源的恶性竞争,乃至于发生死锁。
(3)锁策略
锁策略是指在锁的开销和数据的安全性之间寻求平衡。但是这种平衡会影响性能,所以大多数商业数据库系统没有提供更多的选择,一般都是在表上施加行级锁,并以各种复杂的方式来实现,以便在数据比较多的情况下,提供更好的性能。
表级锁是MySQL中最基本的锁策略,并且是开销最小的策略。它会锁定整张表,一个用户在对表进行操作前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,并且读锁之间是不相互阻塞的。另外,由于写锁比读锁优先级高,所以一个写锁清秋可能会被插入到读锁队列的前面,但是读锁则不能插入到写锁的前面。
行级锁可以最大限度地支持并发处理,同时也带来了最大的锁开销。在InnoDB或者一些其他存储引擎中的锁实现。
(4)锁的生命周期
锁的声明周期是指在一个MySQL会话内,对数据进行加锁到解锁之间的时间间隔。锁的生命周期越长,并发性能就越低,反之并发性能就越高。另外锁是数据库管理系统的重要资源,需要占据一定的服务器内存,锁的周期越长,占用的服务区内存时间就越长;相反占用的内存也就越短。因此,我们应该尽可能地缩短锁的生命周期。
2.MyISAM表的表级锁
MyISAM表不支持COMMIT和ROLLBACK命令。当用户对数据库执行插入、删除、更新等操作时,这些变化的数据都被立刻保存在磁盘中。这样,在多用户环境下,会导致诸多问题。为了避免同一时间有多个用户对数据库中指定表进行操作,可以应用表锁定来避免在用户操作数据表过程中受到干扰。当且仅当该用户释放表的操作锁定后,其他用户才可以访问这些修改后的数据表。
(1)以读方式锁定数据表
首先创建表并且为表加一个读锁。
mysql> create table tb_user( -> id int(10) unsigned NOT NULL auto_increment PRIMARY KEY, -> username varchar(30), -> pwd varchar(30) -> )ENGINE=MyISAM; Query OK, 0 rows affected (0.02 sec) mysql> INSERT INTO tb_user(username,pwd)VALUES -> ('shen','123456'), -> ('lian','123456'), -> ('chen','123456'); Query OK, 3 rows affected (0.00 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql> LOCK TABLE tb_user READ; Query OK, 0 rows affected (0.01 sec) mysql> select * from tb_user; +----+----------+--------+ | id | username | pwd | +----+----------+--------+ | 1 | shen | 123456 | | 2 | lian | 123456 | | 3 | chen | 123456 | +----+----------+--------+ 3 rows in set (0.01 sec)
分别执行两次操作。
mysql> INSERT INTO tb_user(username,pwd)VALUES('tian','123456'); ERROR 1099 (HY000): Table 'tb_user' was locked with a READ lock and can't be updated mysql> UNLOCK TABLES; Query OK, 0 rows affected (0.00 sec) mysql> INSERT INTO tb_user(username,pwd)VALUES('tian','123456'); Query OK, 1 row affected (0.00 sec) mysql> select * from tb_user; +----+----------+--------+ | id | username | pwd | +----+----------+--------+ | 1 | shen | 123456 | | 2 | lian | 123456 | | 3 | chen | 123456 | | 4 | tian | 123456 | +----+----------+--------+ 4 rows in set (0.00 sec)
(2)以写方式锁定数据表
为表添加写锁,然后通过自身命令行读,可以查询表数据,这是因为以写方式锁定数据表并不能限制当前锁定的用户的查询操作。
mysql> LOCK TABLE tb_user WRITE; Query OK, 0 rows affected (0.00 sec) mysql> select * from tb_user; +----+----------+--------+ | id | username | pwd | +----+----------+--------+ | 1 | shen | 123456 | | 2 | lian | 123456 | | 3 | chen | 123456 | | 4 | tian | 123456 | +----+----------+--------+ 4 rows in set (0.00 sec)
再打开一个新用户会话,保持原窗口不被关闭,
然后在原来的会话中打开锁
mysql> UNLOCK tables; Query OK, 0 rows affected (0.00 sec)
新打开的窗口会自动地显示出查询数据。
3.InnoDB表的行级锁
(1)为采用InnoDB存储引擎的数据表tb_account在查询语句中设置读锁
SELECT * FROM tb_account LOCK IN SHARE MODE;
(2)为采用InnoDB存储引擎的数据表tb_account在查询语句中设置写锁
SELECT * FROM tb_account FOR UPDATE;
(3)在更新(包括INSERT UPDATE和DELTET)语句中,InnoDB存储引擎自动为更新语句影响的记录添加隐式写锁。
(4)以上三种方式为表设置行级锁的生命周期非常短暂,为了延长行级锁的生命周期,可以采用开启事务实现。
窗口一:
mysql> use db_bank; Database changed mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM tb_account FOR UPDATE; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 100.00 | | 2 | B | 1900.00 | | 3 | C | 1000.00 | +----+------+---------+ 3 rows in set (0.00 sec)
窗口二:
mysql> use db_bank; Database changed mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM tb_account FOR UPDATE;
窗口一:
mysql> COMMIT; Query OK, 0 rows affected (0.00 sec)
窗口二:
mysql> use db_bank; Database changed mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM tb_account FOR UPDATE; +----+------+---------+ | id | name | balance | +----+------+---------+ | 1 | A | 100.00 | | 2 | B | 1900.00 | | 3 | C | 1000.00 | +----+------+---------+ 3 rows in set (35.82 sec)
4.死锁的概念与避免
死锁就是当两个或者多个处于不同序列的用户打算同时更新某相同的数据库时,因互相等待对方释放权限而导致双方一直处于等待状态。在实际应用中,两个不同序列的客户打算同时对数据执行操作,极有可能产生死锁。更具体地将讲,当两个事物相互等待操作对方释放的所持有的资源,而导致两个事务都无法操作对方持有的资源,这样无限期的等待被称作死锁。
InnoDB表处理程序具有检查死锁这一功能,如果该处理程序发现用户在操作过程中产生死锁,该处理程序立刻通过撤销操作来撤销其中一个事务,以便使死锁消失。
三、事务的隔离级别
锁机制有效地解决了事务的并发问题,但也影响了事务的并发性能。所谓并发是指数据库系统同时为多个用户提供服务的能力。
1.事务的隔离级别与并发问题(4种事务隔离级别)
(1)Serializable(串行化):一个事务在执行过程中首先将其欲操作的数据锁定,待事务结束后释放。如果此时另一个事务也要操纵该数据,必须等待前一个事务释放锁定后才能继续进行。
(2)Repeatable Read(可重复读):一个事务在执行过程中能够看到其他事务已经提交的新插入记录,看不到其他事务对已有记录的修改。
(3)Read Committed(读已提交数据):一个事务在执行过程中能够看到其他事务已经提交的新插入记录,也能看到其他事务已经提交的对已有记录的修改。
(4)Read Uncommitted(读未提交数据):一个事务在执行过程中能够看到其他事务未提交的新插入记录,也能看到其他事务未提交的对已有记录的修改。
2.设置事务的隔离级别(只对当前会话有效)
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; Query OK, 0 rows affected (0.01 sec)