1. 事务简介
事务具有ACID特性
- A 原子性,原子性指整个数据库事务是不可分割的工作单位。
- C 表示一致性,也就是说一致性指事务将数据库从一种状态转变为另一种一致的状态,在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- I 表示隔离性,隔离性也叫做并发控制、可串行化或者锁。
- D 表示持久性,表示事务一旦提交了,其结果就是永久性的。
2. 事务分类
- 扁平事务
- 带有保存点的扁平事务
- 链事务
- 分布式事务
- 扁平事务
扁平事务是最简单的事务,所有操作在同一层次,扁平事务的主要缺点是不能提交或回滚事务的某一部分.
BEGIN WORK
Operation 1
Operation 2
...
Operation n
COMMIT WORK
- 带有保存点的扁平事务
这种事务除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态,这是因为可能某些事务在执行过程中出现的错误并不会对所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点用来通知系统应该记住事务当前的状态,以便以后发生错误时,事务能回到该状态。 - 链事务
链事务,就是指回滚时,只能恢复到最近一个保存点;而带有保存点的扁平事务则可以回滚到任意正确的保存点。 - 嵌套事务
BEGIN WORK
SubTransaction1:
BEGIN WORK
SubOperationX
COMMIT WORK
SubTransaction2:
BEGIN WORK
SubOperationY
COMMIT WORK
...
SubTransactionN:
BEGIN WORK
SubOperationN
COMMIT WORK
COMMIT WORK
- 分布式事务
分布式事务通常是指在一个分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
3. 隐含的commit操作
- DDL语句:
ALTER DATABASE
、ALTER EVENT
、ALTER PROCEDURE
、ALTER TABLE
、ALTER VIEW
、CREATE TABLE
、DROP TABLE
、RENAME TABLE
、TRUNCATE TABLE
。 - 修改MYSQL架构的的语句:
CREATE USER
、DROP USER
、GRANT
、RENAME USER
、REVOKE
、SET PASSWORD
。 - 管理语句:
ANALYZE TABLE
、CACHE INDEX
、CHECK TABLE
、LOAD INDEX INTO CACHE
、OPTIMIZE TABLE
、REPAIRE TABLE
。
4. 事务的隔离级别
隔离级别 | 脏读(Dirty Read) | 不可重复读(NotReplacable Read) | 幻读(Phantom Read) |
---|---|---|---|
未提交读(Read uncommited) | 可能 | 可能 | 可能 |
已提交读(Read commited) | 不可能 | 可能 | 可能 |
可重复读(Repeatable Read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
- 脏读: 一个事务读取到另一个事务没有提交的数据
比如:事务T1更新了一行记录的内容,但是并没有提交所做的修改。事务T2读取到了T1更新后的行,然后T1执行回滚操作,取消了刚才所做的修改。现在T2所读取的行就无效了; - 不可重复读: 在同一事务中,两次读取同一数据,得到内容不同;
比如:事务T1读取一行记录,紧接着事务T2修改了T1刚才读取的那一行记录。然后T1又再次读取这行记录,发现与刚才读取的结果不同。这就称为“不可重复”读,因为T1原来读取的那行记录已经发生了变化; - 幻读:同一事务中,用同样的操作读取两次,得到的记录数不相同;
比如:事务T1读取一条指定的WHERE子句所返回的结果集。然后事务T2新插入 一行记录,这行记录恰好可以满足T1所使用的查询条件中的WHERE子句的条件。然后T1又使用相同的查询再次对表进行检索,但是此时却看到了事务T2刚才插入的新行。这个新行就称为“幻像”,因为对T1来说这一行就像突然出现的一样。
隔离级别越低,事务请求的锁越少或保持锁的时间就越短。InnoDB存储引擎默认的支持隔离级别是REPEATABLE READ;在这种默认的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE级别隔离。
我们可以可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。它的语法如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
注意:默认的行为(不带session和global)是为下一个(未开始)事务设置隔离级别。如果使用GLOBAL关键字,语句在全局对从那点开始创建的所有新连接(除了不存在的连接)设置默认事务级别。你需要SUPER权限来做这个。使用SESSION 关键字为将来在当前连接上执行的事务设置默认事务级别。 任何客户端都能自由改变会话隔离级别(甚至在事务的中间),或者为下一个事务设置隔离级别。
在RC模式下,数据的读取都是不加锁的,但是数据的写入,修改和删除是需要加锁的。
锁的概念
- 共享锁 (S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
- 排它锁 (X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
- 共享锁和排他锁都是行级锁,行级锁对系统开销较大,但处理高并发较好。
- 对于 insert,update,delete 语句 InnoDB 会自动给对应数据加排它锁,对于 select 语句 InnoDB 默认不会加任何锁(因为 MySQL 引入了一种快照读的机制,减少了锁的处理,提升了并发能力),除非显示的给记录加锁。
事例
事务主要处理操作量大,复杂度高的数据。(事务要么全执行,要么全不执行)
开始:START TRANSACTION或BEGIN开始一项事务
提交:COMMIT,提交事务,使变更称为永久变更
回滚:ROLLBACK可以回滚当前事务,取消变更
此外,SET AUTOCOMMIT = {0|1},可以禁用或启用默认的autocommit模式
分析网上购书例子
某书(book_id=123)在数据库中只有一本,而这个时候甲、乙几乎同时对这本书发出购买请求;分析过程。
1、对于用户甲来说,他的动作比乙稍快,其触发的过程如下
* SELECT book_num FROM book WHERE bood_id = 123;
* 当book_num的值大于,确定购买行为并更新book_num
* UPDATE book SET book_num = book_num-1 WHERE book_id = 123;
* 购书成功
2、对于用户乙
* SELECT book_num FROM book WHERE book_id = 123;
* 此时甲刚执行第一步操作,还没有来得及做第二步操作,所以book_num还是 >0
* UPDATE book SET book_num=book_num-1 WHERE book_id = 123
* 购书成功
表面上看来甲,乙购书都成功了,但是数据库都只有一本书,此时怎么可能都买到书呢?看下数据库,
发现book_num=-1,这当然是不允许的。
这个时候我们可以用事务
BEGIN;
SELECT book_num FROM book WHERE book_id = 123;
....
UPDATE book SET book_num=book_num-1 WHERE book_id=123;
COMMIT;
此时可以解决问题吗?当然不能。实际应该如下
BEGIN;
SELECT book_num FROM book WHERE book_id = 123 FOR UPDATE;
....
UPDATE book SET book_num=book_num-1 WHERE book_id=123;
COMMIT;
加上FOR UPDATE的时候会加上一个行锁,如果此时事务没有完全结束,那么其他的事务在使用SELECT .... FOR UPDATE请求的时候就会处于等待状态。
悲观锁和乐观锁
- 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
- 在数据库中,悲观锁的流程如下:
在对任意记录进行修改前,先尝试为该记录加上[排他锁](exclusive locking)。
如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。
如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
- 乐观锁
在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。
乐观锁基本上是基于版本号实现的,一般通过为数据库表中添加一个"version"字段来实现。读取出数据时,将版本号一同读出,之后更新时,对此版本号➕1,此时,将提交数据的版本数据与数据库表对应记录的当前版本号进行比较,如果提交的数据版本号>数据库当前版本号,则更新数据,否则则认为是过期数据。
MVCC在MySQL的InnoDB中的实现
在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读Repeatable reads事务隔离级别下:
- SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
- INSERT时,保存当前事务版本号为行的创建版本号
- DELETE时,保存当前事务版本号为行的删除版本号
- UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行。
通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。
Mysql的InnoDB设置锁属性
- Mysql的InnoDB默认使用的是RR级别,我们可以设置某个session的级别
SET session transaction isolationg level read commited;
SET session binlog_format= 'ROW'
读和读的区别
MySQL中的读,和事务隔离级别中的读,是不一样的。
在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。
对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:
- 快照读:就是select
select * from table ....;
- 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
select * from table where ? lock in share mode; #共享锁
select * from table where ? for update; #排他锁
insert;
update ;
delete;
事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。
写("当前读")
为了解决当前读中的幻读问题,MySQL事务使用了Next-Key锁。
Next-Key锁是行锁和GAP(间隙锁)的合并,行锁上文已经介绍了,接下来说下GAP间隙锁。
行锁可以防止不同事务版本的数据修改提交时造成数据冲突的情况。但如何避免别的事务插入数据就成了问题。