事务
事务属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
- 原子性(Atomicity) :事务是-一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
- 一致性(Consistent) :在事务开始和完成时,数据都必须保持- -致状态。这意味着所有相关的数据规则都必须应用于事务的修改以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
- 隔离性(Isolation) :数据库系统提供一- 定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
不考虑事务的隔离性,引发的安全性问题
脏读:一个事务读到了另一个事务未提交的数据.
- 不可重复读:一个事务读到了另一个事务已经提交(update)的数据.引发一个事务中的多次查询结果不一致.
- 虚读/幻读:一个事务读到了另一个事务已经提交的(insert/delete)数据.导致多次查询的结果不一致**
脏读是事务B里面修改了数据,幻读是事务B里面新增了数据。
(mysql所有事务隔离级别在数据库层面上均可避免.)
事务的隔离级别
读数据一致性及允许的并发副作用隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
未提交读( Read uncommitted ) | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
已提交读( Read comnitted ) | 语句级 | 否 | 是 | 是 |
可重复读( Repeatable read ) | 事务级 | 否 | 否 | 是 |
可序列化( Serializable ) | 最高级别,事务级 | 否 | 否 |
MySQL 在REPEATABLE READ 隔离级别下,是可以禁止幻读问题的发生的。
- 安全性: serializable > repeatable read > read committed > read uncommitted
- 效率 : serializable< repeatable read < read committed < read uncommitted
- MYSQL : repeatable read
-
mvcc
MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程。可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同, READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView。
- REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
版本链
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):
- trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记录修改前的信息。
ReadView
对于使用READ UNCOMMITTED隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录。对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,就需要用到我们上边所说的版本链了,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。
ReadView中主要包含4个比较重要的内容:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
creator_trx_id:表示生成该ReadView的事务的事务id。
注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1, max_trx_id的值就是4。
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
读锁与写锁
- 读锁:共享锁、Shared Locks、S锁。
- 写锁:排他锁、Exclusive Locks、X锁。 | | | | | —- | —- | —- | | X锁 | 冲突 | 冲突 | | S锁 | 冲突 | 不冲突 |
读操作
select …lock in share mode; (显示锁)
将查找到的数据加上一个S锁,允许其他事务继续获取这些记录的S锁,不能获取这些记录的X锁(会阻塞)
select … for update; (显示锁)
将查找到的数据加上一个X锁,不允许其他事务获取这些记录的S锁和X锁。
写操作
- DELETE:删除一条数据时,先对记录加X锁,再执行删除操作。
- INSERT:插入一条记录时,会先加隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问到。
- UPDATE
- 如果被更新的列,修改前后没有导致存储空间变化,那么会先给记录加X锁,再直接对记录进行修改。
- 如果被更新的列,修改前后导致存储空间发生了变化,那么会先给记录加X锁,然后将记录删掉,再Insert一条新记录。
隐式锁:一个事务插入一条记录后,还未提交,这条记录会保存本次事务id,而其他事务如果想来读取这个记录会发现事务id不对应,所以相当于在插入一条记录时,隐式的给这条记录加了一把隐式锁。
行锁范围分类
- LOCK_REC_NOT_GAP: 单个行记录上的锁。行锁
- LOCK_GAP:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读 ,出现幻读的情况。(间隙锁会锁从你查的id到最近小的id之间会锁住)
LOCK_ORDINARY:(Next_Key) 锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。
悲观锁、乐观锁
悲观锁: 用的就是数据库的行锁,认为数据库会发生并发冲突,直接上来就把数据锁住,其他事务不能修改,直至提交了当前事务。
乐观锁: 其实是一种思想,认为不会锁定的情况下去更新数据,如果发现不对劲,才不更新(回滚)。在数据库中往往添加一个version字段来实现。
避免死锁
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁。
- 尽量控制事务大小,减少锁定资源量和时间长度
- 合理设计索引,尽量缩小锁的范围
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率降
- 尽可能低级别事务隔离