1. 锁
InnoDB存储引擎不需要锁升级,因为一个锁和多个锁的开销是相同的,实际上,只有实现本身会增加开销时,行级锁才会增加开销。
1.1 什么是锁
操作缓冲池中的LRU列表,删除、添加、移动LRU列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
1.2 lock和latch
Latch作为一款轻量级的锁,要求锁定的时间必须非常短,在InnoDB存储引擎中,分为mutex(互斥锁)和rwlock(读写锁)。目的是保证并发线程操作临界资源的正确性,并且通常没有死锁检测机制。
Lock的对象是事务,对象如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放。
| Lock | Latch |
对象 | 事务 | 线程 |
保护 | 数据库内容 | 内存数据结构 |
持续时间 | 整个人事务过程 | 临界资源 |
模式 | 行锁、表锁、意向锁 | 读写锁、互斥量 |
死锁 | 通过waits-for graph、time out等机制进行死锁检测与处理 | 无死锁检测与处理机制 |
存在于 | Lock manager的哈希表中 | 每个数据结构的对象中 |
1.3 InnoDB存储引擎中的锁
1.3.1 锁的类型
InnoDB存储引擎实现如下两种标准的行级锁:
- 共享锁:允许事务读一行数据。
- 排他锁:允许事务删除或更新一行数据。
如果一个事务T1获得行r的共享锁,那么事务T2也可以获得行r的共享锁,但事务T3想获得行r的排它锁,得必须等待T1,T2释放行r的共享锁。
1.3.2 一致性非锁定读
如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待上锁的释放,而是去读取一个快照数据。
图1-1 InnoDB存储引擎非锁定的一致性
在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
1.4 锁的算法
1.4.1 行锁的3种算法
- Record lock:单个行记录上的锁
- Grap lock: 间隙锁,锁定一个范围,但不包括记录本身
- Next-key lock:锁定一个范围,并且锁定记录本身
1.4.2 phantom problem
phantom problem是指同一个事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。
在默认的事务隔离级别下,即REPEATABLE READ 下,InnoDB存储引擎采用next-key locking机制来避免phantom problem(幻像问题)
1.5 锁问题
通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作。锁提高了并发,但会带来三种问题,如果可以预防这三种情况的发生,那将不会产生并发异常。
1.5.1 脏读
脏页是指在缓冲池已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,在刷到磁盘之前,日志都已被写入到了重做日志文件中。脏数据是指事物对缓冲池行记录的修改,并且还没有被提交。
脏页是数据库实例内存与磁盘的异步造成的,并不影响数据的一致性,但脏数据一个事务可能是另外一个事务未提交的数据,这显然违反了数据库的隔离性。
1.5.2 不可重复读
不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的是却是已经提交的数据,但是这违反了数据库一致性的要求。但这个是可以接受的,很多数据库厂商将默认隔离级别设置为READ COMMITTED,这种隔离级别下允许不可重复读的现象。
在InnoDB存储引擎中,可通过Next-Key Lock算法避免不可重读的现象。
1.5.3 丢失更新
简单来说就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。避免丢失更新的发生,需要让这种情况的操作变成串行化,即加上排他锁。
1.6 死锁
死锁是指两个或以上的事务在执行过程中,因争夺锁资源而造成的相互等待的现象,InnoDB存储引擎采用wait-for-graph(等待图)的方式解决。等待图要求数据库保存以下两种信息;
- 锁的信息链表
- 事务等待链表
等待图是一种较为主动的死锁检测机制,在每一个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说,InnoDB存储引擎选择回滚undo量最小的事务。
2 悲观锁和乐观锁
2.1 基本概念
乐观锁( Optimistic Locking):顾名思义,对加锁持有一种乐观的态度,即先进行业务操作,不到最后一步不进行加锁,"乐观"的认为加锁一定会成功的,在最后一步更新数据的时候再进行加锁。
悲观锁(Pessimistic Lock):正如其名字一样,悲观锁对数据加锁持有一种悲观的态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
2.2 实现方式
悲观锁的实现:通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select .... for update来实现悲观锁。
begin;
//开启事务,查询要卖的商品,并对该记录加锁。
select nums from tb_goods_stock where goods_id = {$goods_id} for update;
//判断商品数量是否大于购买数量。如果不满足,就回滚事务。
//如果满足条件,则减少库存,并提交事务。
update tb_goods_stock set nums = nums - {$num} where goods_id = {$goods_id} and nums >= {$num};
commit;
注意,使用悲观锁,需要关闭mysql的自动提交功能,将 set autocommit = 0;
乐观锁的实现:给表加一个版本号或时间戳的字段,读取数据时,将版本号一同读出,数据更新时,将版本号加1。当我们提交数据更新时,判断当前的版本号与第一次读取出来的版本号是否相等。如果相等,则予以更新,否则认为数据过期,拒绝更新,让用户重新操作。
begin;
//查询要卖的商品,并获取版本号。
select nums, version from tb_goods_stock where goods_id = {$goods_id};
//判断商品数量是否大于购买数量。如果不满足,就回滚事务。
//如果满足条件,则减少库存。(更新时判断当前version与第1步中获取的version是否相同)
update tb_goods_stock set nums = nums - {$num}, version = version + 1 where goods_id = {$goods_id} and version = {$version} and nums >= {$num};
//判断更新操作是否成功执行,如果成功,则提交,否则就回滚。
commit;
2.3 使用场景
读取频繁使用乐观锁,写入频繁使用悲观锁。乐观锁不能解决脏读的问题。