InnoDB 使用 Buffer Pool 管理内存数据页,如果 Buffer Pool 命中率足够高,那么大部分时候是不需要读磁盘的,直接从内存拿结果,可以加快查询速度。 执行 show engine innodb status ,可以看到“Buffer pool hit rate”字样,显示的就是当前的命中率,一般一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在 99% 以上。 Buffer Pool 的空间是有限的,新旧页面的更替是通过 LRU 算法控制的,但 InnoDB 中的 LRU 并不是单纯的新页面替换老页面(因为这样相当于每次大查询都会把整个 Buffer Pool 都刷新一遍),而是将 LRU 链表分成了 young 区和 old 区,页面第一次被访问时会被添加到 old 区,old 区的页面如果是短期内被多次访问,则其不会被移动到链表的头部(young 区),会很快被淘汰掉。
临时表
1 2 3 4
create temporary table temp_t like t1; alter table temp_t add index(b); insert into temp_t select * from t2 where b>=1 and b<=2000; select * from t1 join temp_t on (t1.b=temp_t.b);
public class MyReadHandler implements SimpleChannelHandler { public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) { Object message = evt.getMessage(); // Do something with the received message. ... // And forward the event to the next handler. ctx.sendUpstream(evt); } }
同时你也可以定义一种操作响应其他处理器的写操作请求:
1 2 3 4 5 6 7 8 9
public class MyWriteHandler implements SimpleChannelHandler { public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) { Object message = evt.getMessage(); // Do something with the message to be written. ... // And forward the event to the next handler. ctx.sendDownstream(evt); } }
封锁是数据库原理范畴内的概念,封锁分为排它锁、共享锁、活锁、死锁这4种,以及封锁协议定义了数据库如何使用这些锁来保证并发安全。 所谓封锁就是事务在对某个数据对象例如表、记录等操作之前,先向系统发出请求对其加锁。 加锁后事务 T 就对该数据对象有了一定的控制,在事务 T 释放它的锁之前,其他事务不能更新此数据对象。例如,事务 T1 要修改 A,若在读出 A 之前先锁住 A,其他事务就不能再读取和修改 A 了,直到 T1 修改并写回 A 后解除了对 A 的封锁为止。这样,就不会丢失 T1 的修改。 确切的控制由封锁的类型决定。基本的封锁类型有两种:排他锁(exclusive locks,简称 X 锁)和共享锁(share locks,简称 S 锁)
**X 锁(排他写锁)**:若事务 T1 对数据对象 A 加上 X 锁,则只允许 T 读取和修改 A,其他任何事物都不能再对 A 加任何类型的锁,直到 T 释放 A 上的锁为止。这就保证了其他事务在 T 释放 A 上的锁之前不能再读取和修改 A;
**S 锁(共享读锁)**:若事务 T 对数据 A 加上 S 锁,则事务 T 可以读 A 但是不能修改 A,其他事务只能对 A 加 S 锁而不能加 X 锁,直到 T 释放 A 上的 S 锁为止。这就保证了其他食物可以读 A,但在 T 释放 A 上的 S 锁之前不能对 A 进行任何修改。
封锁种类
排它锁(X锁) 可读可写,一个事务对表加了X锁,其他事务必须等该事务操作完这张表后,才可以对这张表操作。 如果一个事务给表加了 X 锁(意味着该事务要独占这个表),那么: 别的事务不可以继续获得该表的 S 锁 别的事务不可以继续获得该表中的某些记录的 S 锁 别的事务不可以继续获得该表的 X 锁 别的事务不可以继续获得该表中的某些记录的 X 锁
共享锁(S锁) 只读,多个事务可以同时对都某一张表加共享锁 如果一个事务给表加了 S 锁,那么: 别的事务可以继续获得该表的 S 锁 别的事务可以继续获得该表中的某些记录的 S 锁 别的事务不可以继续获得该表的 X 锁 别的事务不可以继续获得该表中的某些记录的 X 锁
封锁协议
封锁有 3 级的封锁协议:
一级封锁协议 事务 T 在对数据对象 A 进行修改之前,必须对其加 X 锁,直至事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK); 在一级加锁协议中,如果仅仅是对数据进行读操作而不进行修改,是不需要进行加锁的。所以只能避免修改丢失而不能避免不可重复读和脏读。
二级封锁协议 在一级加锁协议的基础上增加事务 T 在读取数据 R 之前必须先对其加 S 锁,读完后即可释放 S 锁; 二级加锁协议除防止了丢失修改,还可进一步防止读脏数据。例如:事务 T1 正在对数据对象 R 进行修改,此前已经对 R 加上了 X 锁,此时事务 T2 想读取 R,就必须对 R 加上 S 锁,但是 T2 发现 R 已经被 T1 加上了 X 锁,于是 T2 只能等待 T1 释放了在 R 上加的锁之后才能对 R 加 S 锁并读取。这能防止 T2 读取到 T1 未提交的数据,从而避免了脏读。 但是在二级封锁协议中,由于读完数据后即可释放 S 锁,所以它不能保证可重复读。
三级封锁协议 三级封锁协议是指,在一级封锁协议的基础上增加事务 T 在读取数据 R 之前对其加 S 锁直至事务结束才释放。 三级封锁协议除了防止丢失修改和读“脏”数据之外,还进一步防止了不可重复读。 上述三级协议的主要区别在于什么操作需要申请加锁,以及何时释放锁(即锁的持有时间)。不同的封锁协议使事务达到的一致性是不同的,封锁协议越高,一致性程度越强。
MySQL 中的锁主要分为闩锁(latch)和锁(lock)。 latch对象是除了数据库对象外的其他对象,包括操作缓冲池汇总的LRU列表、删除、添加、移动LRU列表中的元素,为了保证一致性必须要有锁介入,因此引入了latch锁。latch是轻量级的锁,因为它要求锁定的时间必须非常短,主要用于保护临界资源的线程安全。 lock对象是事务,用来锁定数据库中的对象,如表、行、页。一般lock的对象仅在事务commit或rollback后进行释放。lock有死锁机制。
该锁的官方类型名为 LOCK_REC_NOT_GAP。 和前面提到的表锁一样,分 S 锁和 X 锁,只是作用粒度精确到行了。
Gap Locks(间隙锁)
该锁的官方类型名为 LOCK_GAP。 MySQL 解决幻读问题有两种方案: 第一种是 MVCC,因为新插入的数据事务 ID 必然不在 ReadView 内,因此读取这些记录后会被直接忽略,但是快照读只在普通读操作中生效,如果发生了当前读仍然会有幻读问题; 第二种是加锁,但是加锁有一个问题,就是事务没法给尚不存在的记录加锁。 如果我们希望为 number 值为 8 的记录加 gap 锁,则该记录的前后间隙都不允许别的事务立即插入记录:
如图中为 number 值为 8 的记录加了 gap 锁,意味着不允许别的事务在 number 值为 8 的记录前边的间隙插入新记录,其实就是 number 列的值(3, 8)这个区间的新记录是不允许立即插入的。比方说有另外一个事务再想插入一条 number 值为 4 的新记录,它定位到该条新记录的下一条记录的 number 值为 8,而这条记录上又有一个 gap 锁,所以就会阻塞插入操作,直到拥有这个 gap 锁的事务提交了之后,number 列的值在区间(3, 8)中的新记录才可以被插入。 另外,如何为(20, +∞)这个区间加 gap 锁?其实是为数据页中的 Infimum 和 Supremum 记录加上了 gap 锁。 比如假设此时表里有 5、25 这两条数据,则SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE查询 10 到 20 范围内的记录,并加上范围(5, 10)、[10, 20]、(20, 25]以内的 gap 锁。 比如假设此时表里有 102、105、107 三个值,则select * from test where n = 105 for update;这个语句会对(102, 105)、(105, 107]这两个区间加 gap 锁。
间隙锁可以通过innodb_locks_unsafe_for_binlog参数来开启关闭:
设置为ON,表示关闭区间锁,此时一致性会被破坏(所以是unsafe)
设置为OFF,表示开启区间锁
Next-Key Locks
该锁的官方类型名为 LOCK_ORDINARY。 Next-Key Lock 其实是Record Lock 和 Gap Lock 的组合,它既会保护该条记录,又能阻止别的事务将新记录插入被保护记录的前后间隙。
意向锁
加表锁时怎么知道该表上有没有行锁?InnoDB 通过意向锁(Intention Locks)来解决这个问题: 1、意向共享锁,英文名:Intention Shared Lock,简称IS 锁。当事务准备在某条记录上加 S 锁时,需要先在表级别加一个 IS 锁。 2、意向独占锁,英文名:Intention Exclusive Lock,简称IX 锁。当事务准备在某条记录上加 X 锁时,需要先在表级别加一个 IX 锁。
IS、IX 锁是表级锁,它们的提出仅仅为了在之后加表级别的 S 锁和 X 锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实 IS 锁和 IX 锁是兼容的,IX 锁和 IX 锁是兼容的。
如上图所示,有 3 个事务,其中 T1 持有 gap 锁,所以 T2 和 T3 需要生成一个插入意向锁的锁结构,等待 T1 释放后才能获取到插入意向锁(本质上是将 is_waiting 属性改成了 false),然后再继续执行插入操作。
隐式锁
一般来说间隙锁可以避免 gap 锁锁住的区间被其他事务修改(当要插入的记录所在的区间有 gap 锁,事务会先再该间隙上加一个插入意向锁),但是还有一种情况正好是反过来的:如果一个事务首先插入了一条记录,别的记录如果直接读(SELECT … LOCK IN SHARE MODE 或 SELECT … FOR UPDATE)则会产生脏读问题,如果直接修改则又会产生脏写问题。 这个问题在 InnoDB 中是通过事务 ID 解决的:
聚簇索引中有一个隐藏列 trx_id,存储的是最后改动该记录的事务 ID,新插入记录的 trx_id 当然就是当前事务的事务 ID,如果别的事务想对该记录添加 S 锁或 X 锁,会首先看一下该记录 trx_id 是否是当前正活跃的事务,如果是的话就会创建一个 X 锁然后进入等待状态;
读-读的情况并不会引起并发冲突,我们不希望对这种情况造成影响,因此 MySQL 给锁分了几个类: 1、共享锁(Shared Locks、S 锁):在事务要读取一条记录时,需要先获取该记录的 S 锁。 2、独占锁(排他锁、Exclusive Locks、X 锁):在事务要改动一条记录时,需要先获取该记录的 X 锁。
兼容性
X
S
X
不兼容
不兼容
S
不兼容
兼容
一般读取一条记录时我们会获取这条记录的 S 锁,但是如果我们想在读取记录时就获取记录的 X 锁,来禁止别的事务读写该记录,则需要使用一些特殊的 SELECT 语句格式: 1、对读取的记录加 S 锁
1
SELECT ... LOCK IN SHARE MODE;
上面语句为记录加 S 锁,允许多个事务同时发起读请求,但是当别的事务尝试获取 X 锁(SELECT … FOR UPDATE 或修改这些记录)则会阻塞,直到当前事务提交之后将这些记录上的 S 锁释放掉。 2、对读取的记录加 X 锁
1
SELECT ... FOR UPDATE;
上面语句为记录加 X 锁,之后别的事务来获取该记录的 S 锁或 X 锁时都会被阻塞,直到当前事务提交之后将这些记录上的 X 锁释放掉。
写操作(write)如何利用锁
DELETE
DELETE 操作会先在 B+树中定位到这条记录,然后获取这条记录的 X 锁,并执行 delete mark 操作(逻辑删除)。
UPDATE
如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在 B+树种定位到这条记录然后获取该记录的 X 锁,最后在原记录的位置执行修改操作。 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在 B+树种定位到这条记录后获取 X 锁,将这条记录彻底删除(移入垃圾链表),最后插入一条记录。 如果修改了该记录的键值,则相当于在原记录上 DELETE 后 INSERT。
因此 Session B 要往这个间隙中插入 id = 8 会被锁住,但是 Session C 修改 id = 10 这行是可以的。
例 2、主键不明确导致锁表
下面的情况不会锁表:
1 2 3 4 5 6
明确指定主键,并且有此行数据,row lock SELECT * FROM products WHERE id = '3' FOR UPDATE; SELECT * FROM products WHERE id = '3' and type = 1 FOR UPDATE; 明确指定主键,若查无此行数据,无lock SELECT * FROM products WHERE id = '-1' FOR UPDATE;
下面的情况会锁表:
1 2 3 4 5 6
非索引字段,table lock SELECT * FROM products WHERE name = 'Mouse' FOR UPDATE; 主键不明确,table lock SELECT * FROM products WHERE id <> '3' FOR UPDATE; 主键不明确,table lock SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
顺序封锁法:顺序封锁法是预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实施封锁。例如在 B 树结构的索引中,可规定封锁的顺序必须是从根节点开始,然后是下一级的子节点,逐级封锁。顺序封锁法可以有效地避免死锁,但是要实现顺序封锁法十分的困难,因为很难事先确定每一个事务要封锁哪些对象,因此也就很难按规定的顺序去实施加锁。
MySQL有死锁检测机制,因此B和C中有一个会插入成功,而另一个事务会自动放弃,并报错:Error Code: 1213. Deadlock found when trying to get lock; try restarting transaction
死锁示例3 - 并发间隙锁的死锁
1 2 3 4 5 6 7 8
A:set session autocommit=0; A:start transaction; A:delete from t where id=6; B:set session autocommit=0; B:start transaction; B:delete from t where id=7; A:insert into t values(5); B:insert into t values(8);
insert into t values(1,1,'2018-11-13'); insert into t values(2,2,'2018-11-12'); insert into t values(3,3,'2018-11-11'); insert into t values(4,4,'2018-11-10'); insert into t values(5,5,'2018-11-09');
delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;
服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务 id 时,就会把该变量的值当作事务 id 分配给该事务,并且把该变量自增 1。
每当这个变量的值为 256 的倍数时,就会将该变量的值刷新到系统表空间的页号为 5 的页面中一个称之为 Max Trx ID 的属性处,这个属性占用 8 个字节的存储空间。
当系统下一次重新启动时,会将上边提到的 Max Trx ID 属性加载到内存中,将该值加上 256 之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于 Max Trx ID 属性值)。
undo log 格式
1、查看 table id 记录 undo log 时会使用到表的 table id,这个值可以通过SELECT * FROM information_schema.innodb_sys_tables WHERE name = 'database_name/undo_demo';这条命令来查看 2、INSERT 操作的 undo log
undo no 在一个事务中是从 0 开始递增的,也就是说只要事务没提交,每生成一条 undo 日志,那么该条日志的 undo no 就增 1。 主键可能是有多个列组成的,如果有多个列,则每个列占用的存储空间大小和对应的真实值都需要记录下来。 比如对下面这条插入了两条记录的 SQL: