hbprotoss

hbprotoss 查看完整档案

填写现居城市北京理工大学  |  软件工程 编辑网易  |  Java 编辑 hbprotoss.github.io 编辑
编辑

May you find your worth in the waking world

个人动态

hbprotoss 发布了文章 · 2019-10-21

MySQL组复制-初见

自从MySQL组复制(Group Replication)跟随5.7.17发布以来,MySQL真正有了成熟的高可用分布式集群方案,摆脱了之前master-slave+第三方工具的伪集群。笔者将整理关于MySQL组复制的系列文章,这是第一篇,简要介绍组复制的来世今生以及相关特性,希望对大家有帮助

简介

在介绍组复制之前,让我们先回顾一下传统的master-slave复制形式,这将有利于我们理解组复制要解决什么问题

1.png

传统复制

传统的MySQL复制形式为主从复制,由一个主库(master)和一个或多个从库(slave)组成。事务只能在主库执行,主库执行事务,提交(commit)之后,将会传播给从库执行。一般这个过程是异步的,由binlog提供基于语句的复制(实际执行的sql),或者基于行的复制(增删改的行sql)。这是一个shared-nothing的系统,所有主从都有一套完整独立的数据。

由于纯异步复制可能会造成数据丢失(以后详细分析),5.5中以插件形式引入了半同步复制,在主库向客户端返回事务已提交的信号之前,多了一个同步步骤,要等从库通知主库事务已接收。这个同步可以发生在server层sync binlog之后、innodb引擎commit之前(after sync),或者在innodb引擎commit之后、返回给客户端之前(after commit)。

组复制

组复制是一种可以用来实现高可用集群的技术,组内的各个服务器相互通信协作,来保证事务的ACID特性。组复制实现了多主写入的特性,即,任意一个主库都可以做数据的更新,事务会被复制到其余的主库以及从库中。在被写入的主库返回给客户端事务已提交之前,组复制插件会保证将被写入的数据,以及写集(write set,写入数据的唯一标识符,一般为主键)有序传播到其他的主库中。注意,这个“有序”非常重要,它将保证所有主库接收到的都是一致的事务,而不至于发生数据错乱。

显而易见的是,多主写入的特性下,多个客户端并发更新数据,必然导致事务冲突,如何解决冲突就显得尤为重要。这个时候,写集(write set)的作用就体现出来了,如果在不同主库上的两个事务,更新了同一行数据,它们将产生相同的写集,MySQL就能检测到事务冲突。MySQL解决冲突的策略是,谁先提交以谁为主,后提交的事务回滚,这样来保证数据的一致性。

和传统复制一样,组复制也是一个shared-nothing的系统,所有主库都有同等的完整数据。

组复制不解决数据分片问题

组复制细节

组复制整个逻辑是实现在MySQL插件里的,作为传统过复制框架的一种扩展,基于binlog、GTID,和一个第三方的组内通讯组件Corosync by corosync实现。除此之外,组复制还实现了动态变更机制,新机器可以动态加入一个一存在的组复制集内。

组:复制的基本单位

在组复制中,相互复制的几组服务器组成了一个基本单位:组(group)。一个组用uuid作为组名,新的服务器可以自动加入到一个组中,以传统的复制方式复制数据,从而和现有服务器的数据保持一致,无需人工干预。
当一个服务器从组中下线时,其他服务器会自动感知这个下线事件,并作出相应调整

多点写入

由于不存在传统意义上的主库,组中的任意一个节点都可以用来执行事务,包括写入类事务。就像前面提到过的,在事务提交前,要做一些额外的检查工作:

  1. 检查事务是否有冲突
  2. 将事务传播给其他节点,其他节点需要确认已接收到事务

解决冲突的时候,组复制遵循先到先的的原则。例如,t1/t2两个事务,逻辑上t1先提交,然而在t2的节点上,t2被先执行(本地事务)。当t1事务被广播到t2后,t2所在的节点检测到两个事务写集(write set)冲突,并且t1先提交。那么根据原则,t2所在的节点将回滚t2事务,执行t1。

实践中,如果频繁发生本地事务被远程事务回滚,那么建议将两个事务放同一个节点执行,由本地的锁管理器控制并发问题,而不是等事务传播过来之后,再解决冲突。

监控

和其他指标一样,组复制的监控数据可以从performance_schema中获取,包括组的节点信息、冲突统计、服务状态等。虽然组复制是一个分布式架构,但是监控数据可以从任意一个mysql节点中获取。

例如,要想获取组中的节点信息,可以做如下查询

member1> SELECT * FROM performance_schema.replication_group_members\G
*************************** 1. row ***************************
 CHANNEL_NAME: group_replication_applier
 MEMBER_ID: 597dbb72-3e2c-11e4-9d9d-ecf4bb227f3b
 MEMBER_HOST: nightfury
 MEMBER_PORT: 13000
 MEMBER_STATE: ONLINE
*************************** 2. row ***************************
 CHANNEL_NAME: group_replication_applier
 MEMBER_ID: 59efb8a1-3e2c-11e4-9d9d-ecf4bb227f3b
 MEMBER_HOST: nightfury
 MEMBER_PORT: 13001
 MEMBER_STATE: ONLINE
*************************** 3. row ***************************
 CHANNEL_NAME: group_replication_applier
 MEMBER_ID: 5a706f6b-3e2c-11e4-9d9d-ecf4bb227f3b
 MEMBER_HOST: nightfury
 MEMBER_PORT: 13002
 MEMBER_STATE: RECOVERING

事务信息(等待执行、事务冲突等)

member1> SELECT * FROM performance_schema.replication_group_member_stats\G
*************************** 1. row ***************************
 CHANNEL_NAME: group_replication_applier
 VIEW_ID: 1428497631:1
 MEMBER_ID: e38fdea8-dded-11e4-b211-e8b1fc3848de
 COUNT_TRANSACTIONS_IN_QUEUE: 0
 COUNT_TRANSACTIONS_CHECKED: 12
 COUNT_CONFLICTS_DETECTED: 5
 COUNT_TRANSACTIONS_VALIDATING: 6
 TRANSACTIONS_COMMITTED_ALL_MEMBERS: 8a84f397-aaa4-18df-89ab-c70aa9823561:1-7
 LAST_CONFLICT_FREE_TRANSACTION: 8a84f397-aaa4-18df-89ab-c70aa9823561:7

组复制内部架构

组复制作为一个MySQL插件实现,内部复用了很多现有的代码

2.png

插件跟MySQL server层和复制架构层高度集成,利用了现有的binlog、relay log、GTID、从库复制线程、applier等,所以熟悉MySQL的读者能从中发现很多熟悉的东西,没有什么太多的区别。但是于此同时,为了能和其他组件更好的协作,MySQL内部做了一系列重构,模块化抽象等。

桔红色的为插件层,从图上可以看出,最上层为和server层交互的接口,在事务执行的各个阶段都有留下hook,插件能感知事物生命周期的各个阶段。反过来,通过这些接口,插件也能控制MySQL的行为,如什么时候能真正向客户端返回事务已提交。

接口层往下为各具体功能模块。Capture模块用来跟踪记录事物实行的上下文;Applier模块用来应用(执行)远程事务,类似从库的apply线程;Recovery模块用来实现节点的恢复功能,或者帮助新节点和现有节点同步等。

再往下是复制协议层,包括了冲突检测、事务接收、传播等复制相关的逻辑。

最底下是组通信的API层,封装了corosync的一些细节,能更方便的被上层调用。最下面的紫色部分是corosync逻辑,实现了组内通信。

组复制高可用

一般来说,一个能够容错的系统,会利用冗余、复制,将一个节点的状态复制到其他独立节点上,即使其中的某些节点宕机,整个系统依旧可用(虽然可能会有降级,比如性能下降等)

组复制很好的实现了上面提到的特性。故障会被组内通信协议发现并追踪,并隔离到独立节点上。故障恢复协议会被用来自动将节点维持到最新状态。组复制不需要failover,多点写入的特性也保证了节点故障也不会导致写入阻塞。

需要注意的是,虽然数据库服务整体是高可用的状态,但是遇到某个节点宕机,和这个节点通信的客户端需要做重连操作。这个不是组复制本身要考虑的事情,客户端连接池、负载均衡器等都可以用来解决这个问题。

另外,当有新节点加入的时候,可能需要一定的时间才能使新节点和现有节点保持同步,建议全量文件同步加增量复制的方式,这将大大降低同步的时间。


本文转载自本人博客http://hbprotoss.github.io/post/mysql组复制-初见/

主要翻译自MySQL高可用团队博客[MySQL Group Replication : Hello World! | MySQL High Availability](

查看原文

赞 0 收藏 0 评论 0

hbprotoss 发布了文章 · 2019-05-18

一个mysql死锁场景分析

最近遇到一个mysql在RR级别下的死锁问题,感觉有点意思,研究了一下,做个记录。
涉及知识点:共享锁、排他锁、意向锁、间隙锁、插入意向锁、锁等待队列

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路


场景

隔离级别:Repeatable-Read
表结构如下

create table t (
    id int not null primary key AUTO_INCREMENT,
    a int not null default 0,
    b varchar(10) not null default '',
    c varchar(10) not null default '',
    unique key uniq_a_b(a,b),
    unique key uniq_c(c)
);

初始化数据

insert into t(a,b,c) values(1,'1','1');

有A/B两个session,按如下顺序执行两个事务
transaction

结果是

  • B执行完4之后还是一切正常
  • A执行5的时候,被block
  • B接着执行6,B报死锁,B回滚,A插入数据

show engine innodb status中可以看到死锁信息,这里先不贴,先解释几种锁的概念,再来理解死锁过程

共享(S)锁/互斥(X)锁

  • 共享锁允许事务读取记录
  • 互斥锁允许事务读写记录

这两种其实是锁的模式可以和行锁、间隙锁混搭,多个事务可以同时持有S锁,但是只有一个事务能持有X锁

意向锁

一种表锁(也是一种锁模式),表明有事务即将给对应表的记录加S或者X锁。SELECT ... LOCK IN SHARE MODE会在给记录加S锁之前先给表加IS锁,SELECT ... FOR UPDATE会在给记录加X锁之前给表加IX锁。
这是一种mysql的锁优化策略,并不是很清楚意向锁的优化点在哪里,求大佬指教

两种锁的兼容情况如下
locks

行锁

很简单,给对应行加锁。比如updateselect for updatedelete等都会给涉及到的行加上行锁,防止其他事务的操作

间隙锁

在RR隔离级别下,为了防止幻读现象,除了给记录本身,还需要为记录两边的间隙加上间隙锁。
比如列a上有一个普通索引,已经有了1、5、10三条记录,select * from t where a=5 for update除了会给5这条记录加行锁,还会给间隙(1,5)和(5,10)加上间隙锁,防止其他事务插入值为5的数据造成幻读。
当a上的普通索引变成唯一索引时,不需要间隙锁,因为值唯一,select * from t where a=5 for update不可能读出两条记录来。

间隙锁相互兼容,因为如果互斥,事务A持有左半段(1,5),事务B持有右半段(1,10),那么当前面那个例子中a=5的记录被删除时,理论上左右两个间隙锁得合并成一个新锁(1,10),那么这个新的大范围锁属于谁呢?所以间隙锁相互兼容,不管是S间隙锁还是X间隙锁

插入意向锁

插入意向锁其实是一种特殊的间隙锁,从前面对间隙锁的描述中可以得知,两个事务在真正insert之前可以同时持有一段间隙的间隙锁,锁不住真正insert的这个动作。真正insert之前,mysql还会尝试获取对应记录的插入意向锁,表明有在间隙中插入一个值的意向。
插入意向锁和间隙锁互斥,比如事务1锁了(1,5)这个间隙,事务2就不能获取到a=3的插入意向锁,所以需要锁等待。

死锁过程分析

接下来就可以来分析前面那个例子中的死锁过程了,先看show engine innodb status

 *** (1) TRANSACTION:
TRANSACTION 5967, ACTIVE 8 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 9, OS thread handle 140528848688896, query id 537 192.168.128.1 root update
insert into t(a,b) values(0,'0')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5967 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 5968, ACTIVE 7 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 8, OS thread handle 140528848484096, query id 538 192.168.128.1 root update
insert into t(a,b) values(0,'0')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5968 lock_mode X locks gap before rec
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5968 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** WE ROLL BACK TRANSACTION (2)

session A(即TRANSACTION 5967)正在等待记录(a=1,b='1')之前的插入意向锁,session B(即TRANSACTION 5968)持有记录(a=1,b='1')之前的间隙锁,却也在等待那个插入意向锁。这说的什么玩意儿,是不是很诡异?

从头开始分析过程

  1. A、B分别begin,开始事务
  2. A先执行select * from t where a=0 and b='0' for update;,先加了IX锁,然后原本意图为给(0, '0')这条记录加排他行锁,但是记录不存在,所以变成了排他间隙锁(-∞,1)
  3. B再执行select * from t where a=0 and b='0' for update;,也是先加了IX锁,因为记录不存在,所以加上了排他间隙锁(-∞,1),但是由于间隙锁相互兼容,所以没有block
  4. A执行insert into t(a,b) values(0,'0');,这时候,要开始真正insert了,A需要获得(0,'0')上的插入意向锁,由于和B持有的(-∞,1)排他间隙锁冲突,所以锁等待,进入记录(0,'0')的锁等待队列(虽然记录并不存在)
  5. B执行insert into t(a,b) values(0,'0');,要获取插入意向锁,发现虽然B自己是持有(-∞,1)的排他间隙锁,但是A也有,所以进入等待队列,等待A释放
  6. 叮,死锁发生

死锁信息解读

事务1(TRANSACTION 5967),等待获得锁index uniq_a_b of table t2.t trx id 5967 lock_mode X locks gap before rec insert intention waiting,即在唯一索引uniq_a_b上的插入意向锁(lock_mode X locks gap before rec insert intention)
锁的边界为

 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

表明两行记录

  • 0和1表示uniq_a_b上的值,a=1,b=0x31(即'1'的ascii码)
  • a=1,b='1'对应的主键id=1,因为innodb的索引结构决定的,二级索引(非主键索引)指向主键索引,主键索引再指向数据,所以需要给主键加索引

至于int值按位或上的0x80000000就不是很清楚为什么了,需要大佬解读

事务2(TRANSACTION 5968),持有间隙锁index uniq_a_b of table t2.t trx id 5968 lock_mode X locks gap before rec,等待插入意向锁index uniq_a_b of table t2.t trx id 5968 lock_mode X locks gap before rec insert intention,所以死锁发生。

原则上是innodb引擎判断哪个事务回滚代价小就回滚哪个事务,但是具体评判标准不是很清楚(再一次需要大佬),这里innodb选择了回滚事务2。至此,死锁过程分析完毕

One More Thing

还没完。。。有个神奇的现象是,如果表结构变成

create table t (
    id int not null primary key AUTO_INCREMENT,
    a int not null default 0,
    b varchar(10) not null default '',
    c varchar(10) not null default '',
    unique key uniq_c(c),
    unique key uniq_a_b(a,b)
);
insert into t(a,b,c) values(1,1,1);

只是把c上的唯一索引uniq_c放到了uniq_a_b前面,那么最后的死锁信息就变了!

 *** (1) TRANSACTION:
TRANSACTION 5801, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 5, OS thread handle 140528848688896, query id 380 192.168.128.1 root update
insert into t2(a,b) values(0,'0')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 56 page no 5 n bits 72 index uniq_a_b of table `t2`.`t2` trx id 5801 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 5802, ACTIVE 4 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 6, OS thread handle 140528848484096, query id 381 192.168.128.1 root update
insert into t2(a,b) values(0,'0')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 56 page no 5 n bits 72 index uniq_a_b of table `t2`.`t2` trx id 5802 lock_mode X locks gap before rec
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 56 page no 4 n bits 72 index uniq_c of table `t2`.`t2` trx id 5802 lock mode S waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 0; hex ; asc ;;
 1: len 4; hex 80000002; asc     ;;

*** WE ROLL BACK TRANSACTION (2)

事务2等待的锁由前面的插入意向锁变成了共享锁。什么鬼?
由于没看过源码,只能根据现象倒推:因为表结构上c的唯一索引在(a,b)前面,而插入的时候没指定c的值,用的默认值0,innodb需要先去查一下有没有0这条记录,有的话就要报唯一键冲突了,所以先要加S锁,但是在(0,'0')这条记录上已经有了IX锁,看一下前面的兼容性矩阵,S锁和IX锁互斥,所以也只能锁等待

总结

看似一句简单的select和insert,底下设计非常复杂的锁机制,理解这些锁机制有利于写出高效的SQL(至少是正确的?)

遗留问题:

  1. 意向锁的优化点是哪
  2. 锁信息里,行记录按位或上的0x80000000是啥
  3. 锁互斥的判定顺序,场景1中,(0,'0')上有兼容的间隙锁,也有等待队列中的锁,先判定哪个?
  4. innodb计算事务回滚代价的算法

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路

参考资料

查看原文

赞 8 收藏 6 评论 0

hbprotoss 发布了文章 · 2019-05-18

一个mysql死锁场景分析

最近遇到一个mysql在RR级别下的死锁问题,感觉有点意思,研究了一下,做个记录。
涉及知识点:共享锁、排他锁、意向锁、间隙锁、插入意向锁、锁等待队列

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路


场景

隔离级别:Repeatable-Read
表结构如下

create table t (
    id int not null primary key AUTO_INCREMENT,
    a int not null default 0,
    b varchar(10) not null default '',
    c varchar(10) not null default '',
    unique key uniq_a_b(a,b),
    unique key uniq_c(c)
);

初始化数据

insert into t(a,b,c) values(1,'1','1');

有A/B两个session,按如下顺序执行两个事务
transaction

结果是

  • B执行完4之后还是一切正常
  • A执行5的时候,被block
  • B接着执行6,B报死锁,B回滚,A插入数据

show engine innodb status中可以看到死锁信息,这里先不贴,先解释几种锁的概念,再来理解死锁过程

共享(S)锁/互斥(X)锁

  • 共享锁允许事务读取记录
  • 互斥锁允许事务读写记录

这两种其实是锁的模式可以和行锁、间隙锁混搭,多个事务可以同时持有S锁,但是只有一个事务能持有X锁

意向锁

一种表锁(也是一种锁模式),表明有事务即将给对应表的记录加S或者X锁。SELECT ... LOCK IN SHARE MODE会在给记录加S锁之前先给表加IS锁,SELECT ... FOR UPDATE会在给记录加X锁之前给表加IX锁。
这是一种mysql的锁优化策略,并不是很清楚意向锁的优化点在哪里,求大佬指教

两种锁的兼容情况如下
locks

行锁

很简单,给对应行加锁。比如updateselect for updatedelete等都会给涉及到的行加上行锁,防止其他事务的操作

间隙锁

在RR隔离级别下,为了防止幻读现象,除了给记录本身,还需要为记录两边的间隙加上间隙锁。
比如列a上有一个普通索引,已经有了1、5、10三条记录,select * from t where a=5 for update除了会给5这条记录加行锁,还会给间隙(1,5)和(5,10)加上间隙锁,防止其他事务插入值为5的数据造成幻读。
当a上的普通索引变成唯一索引时,不需要间隙锁,因为值唯一,select * from t where a=5 for update不可能读出两条记录来。

间隙锁相互兼容,因为如果互斥,事务A持有左半段(1,5),事务B持有右半段(1,10),那么当前面那个例子中a=5的记录被删除时,理论上左右两个间隙锁得合并成一个新锁(1,10),那么这个新的大范围锁属于谁呢?所以间隙锁相互兼容,不管是S间隙锁还是X间隙锁

插入意向锁

插入意向锁其实是一种特殊的间隙锁,从前面对间隙锁的描述中可以得知,两个事务在真正insert之前可以同时持有一段间隙的间隙锁,锁不住真正insert的这个动作。真正insert之前,mysql还会尝试获取对应记录的插入意向锁,表明有在间隙中插入一个值的意向。
插入意向锁和间隙锁互斥,比如事务1锁了(1,5)这个间隙,事务2就不能获取到a=3的插入意向锁,所以需要锁等待。

死锁过程分析

接下来就可以来分析前面那个例子中的死锁过程了,先看show engine innodb status

 *** (1) TRANSACTION:
TRANSACTION 5967, ACTIVE 8 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 9, OS thread handle 140528848688896, query id 537 192.168.128.1 root update
insert into t(a,b) values(0,'0')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5967 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 5968, ACTIVE 7 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 8, OS thread handle 140528848484096, query id 538 192.168.128.1 root update
insert into t(a,b) values(0,'0')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5968 lock_mode X locks gap before rec
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 64 page no 4 n bits 72 index uniq_a_b of table `t2`.`t` trx id 5968 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** WE ROLL BACK TRANSACTION (2)

session A(即TRANSACTION 5967)正在等待记录(a=1,b='1')之前的插入意向锁,session B(即TRANSACTION 5968)持有记录(a=1,b='1')之前的间隙锁,却也在等待那个插入意向锁。这说的什么玩意儿,是不是很诡异?

从头开始分析过程

  1. A、B分别begin,开始事务
  2. A先执行select * from t where a=0 and b='0' for update;,先加了IX锁,然后原本意图为给(0, '0')这条记录加排他行锁,但是记录不存在,所以变成了排他间隙锁(-∞,1)
  3. B再执行select * from t where a=0 and b='0' for update;,也是先加了IX锁,因为记录不存在,所以加上了排他间隙锁(-∞,1),但是由于间隙锁相互兼容,所以没有block
  4. A执行insert into t(a,b) values(0,'0');,这时候,要开始真正insert了,A需要获得(0,'0')上的插入意向锁,由于和B持有的(-∞,1)排他间隙锁冲突,所以锁等待,进入记录(0,'0')的锁等待队列(虽然记录并不存在)
  5. B执行insert into t(a,b) values(0,'0');,要获取插入意向锁,发现虽然B自己是持有(-∞,1)的排他间隙锁,但是A也有,所以进入等待队列,等待A释放
  6. 叮,死锁发生

死锁信息解读

事务1(TRANSACTION 5967),等待获得锁index uniq_a_b of table t2.t trx id 5967 lock_mode X locks gap before rec insert intention waiting,即在唯一索引uniq_a_b上的插入意向锁(lock_mode X locks gap before rec insert intention)
锁的边界为

 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

表明两行记录

  • 0和1表示uniq_a_b上的值,a=1,b=0x31(即'1'的ascii码)
  • a=1,b='1'对应的主键id=1,因为innodb的索引结构决定的,二级索引(非主键索引)指向主键索引,主键索引再指向数据,所以需要给主键加索引

至于int值按位或上的0x80000000就不是很清楚为什么了,需要大佬解读

事务2(TRANSACTION 5968),持有间隙锁index uniq_a_b of table t2.t trx id 5968 lock_mode X locks gap before rec,等待插入意向锁index uniq_a_b of table t2.t trx id 5968 lock_mode X locks gap before rec insert intention,所以死锁发生。

原则上是innodb引擎判断哪个事务回滚代价小就回滚哪个事务,但是具体评判标准不是很清楚(再一次需要大佬),这里innodb选择了回滚事务2。至此,死锁过程分析完毕

One More Thing

还没完。。。有个神奇的现象是,如果表结构变成

create table t (
    id int not null primary key AUTO_INCREMENT,
    a int not null default 0,
    b varchar(10) not null default '',
    c varchar(10) not null default '',
    unique key uniq_c(c),
    unique key uniq_a_b(a,b)
);
insert into t(a,b,c) values(1,1,1);

只是把c上的唯一索引uniq_c放到了uniq_a_b前面,那么最后的死锁信息就变了!

 *** (1) TRANSACTION:
TRANSACTION 5801, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 5, OS thread handle 140528848688896, query id 380 192.168.128.1 root update
insert into t2(a,b) values(0,'0')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 56 page no 5 n bits 72 index uniq_a_b of table `t2`.`t2` trx id 5801 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 5802, ACTIVE 4 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 6, OS thread handle 140528848484096, query id 381 192.168.128.1 root update
insert into t2(a,b) values(0,'0')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 56 page no 5 n bits 72 index uniq_a_b of table `t2`.`t2` trx id 5802 lock_mode X locks gap before rec
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 1; hex 31; asc 1;;
 2: len 4; hex 80000001; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 56 page no 4 n bits 72 index uniq_c of table `t2`.`t2` trx id 5802 lock mode S waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 0; hex ; asc ;;
 1: len 4; hex 80000002; asc     ;;

*** WE ROLL BACK TRANSACTION (2)

事务2等待的锁由前面的插入意向锁变成了共享锁。什么鬼?
由于没看过源码,只能根据现象倒推:因为表结构上c的唯一索引在(a,b)前面,而插入的时候没指定c的值,用的默认值0,innodb需要先去查一下有没有0这条记录,有的话就要报唯一键冲突了,所以先要加S锁,但是在(0,'0')这条记录上已经有了IX锁,看一下前面的兼容性矩阵,S锁和IX锁互斥,所以也只能锁等待

总结

看似一句简单的select和insert,底下设计非常复杂的锁机制,理解这些锁机制有利于写出高效的SQL(至少是正确的?)

遗留问题:

  1. 意向锁的优化点是哪
  2. 锁信息里,行记录按位或上的0x80000000是啥
  3. 锁互斥的判定顺序,场景1中,(0,'0')上有兼容的间隙锁,也有等待队列中的锁,先判定哪个?
  4. innodb计算事务回滚代价的算法

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路

参考资料

查看原文

赞 8 收藏 6 评论 0

hbprotoss 发布了文章 · 2019-05-18

从实现角度看redis lazy free的使用和注意事项

本文主要从实现角度分析了redis lazy free特性的使用方法和注意事项

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路


众所周知,redis对外提供的服务是由单线程支撑,通过事件(event)驱动各种内部逻辑,比如网络IO、命令处理、过期key处理、超时等逻辑。在执行耗时命令(如范围扫描类的keys, 超大hash下的hgetall等)、瞬时大量key过期/驱逐等情况下,会造成redis的QPS下降,阻塞其他请求。近期就遇到过大容量并且大量key的场景,由于各种原因引发的redis内存耗尽,导致有6位数的key几乎同时被驱逐,短期内redis hang住的情况

耗时命令是客户端行为,服务端不可控,优化余地有限,作者antirez在4.0这个大版本中增加了针对大量key过期/驱逐的lazy free功能,服务端的事情还是可控的,甚至提供了异步删除的命令unlink(前因后果和作者的思路变迁,见作者博客:Lazy Redis is better Redis - <antirez>

lazy free的功能在使用中有几个注意事项(以下为个人观点,有误的地方请评论区交流):

  1. lazy free不是在遇到快OOM的时候直接执行命令,放后台释放内存,而是也需要block一段时间去获得足够的内存来执行命令
  2. lazy free不适合kv的平均大小太小或太大的场景,大小均衡的场景下性价比比较高(当然,可以根据业务场景调整源码里的宏,重新编译一个版本)
  3. redis短期内其实是可以略微超出一点内存上限的,因为前一条命令没检测到内存超标(其实快超了)的情况下,是可以写入一个很大的kv的,当后续命令进来之后会发现内存不够了,交给后续命令执行释放内存操作
  4. 如果业务能预估到可能会有集中的大量key过期,那么最好ttl上加个随机数,匀开来,避免集中expire造成的blocking,这点不管开不开lazy free都一样

具体分析请见下文

参数

redis 4.0新加了4个参数,用来控制这种lazy free的行为

  • lazyfree-lazy-eviction:是否异步驱逐key,当内存达到上限,分配失败后
  • lazyfree-lazy-expire:是否异步进行key过期事件的处理
  • lazyfree-lazy-server-del:del命令是否异步执行删除操作,类似unlink
  • replica-lazy-flush:replica client做全同步的时候,是否异步flush本地db

以上参数默认都是no,按需开启,下面以lazyfree-lazy-eviction为例,看看redis怎么处理lazy free逻辑,其他参数的逻辑类似

源码分析

命令处理逻辑

int processCommand(client *c)是redis处理命令的主方法,在真正执行命令前,会有各种检查,包括对OOM情况下的处理

int processCommand(client *c) {
    // ...

    if (server.maxmemory && !server.lua_timedout) {
        // 设置了maxmemory时,如果有必要,尝试释放内存(evict)
        int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;

        // ...

        // 如果释放内存失败,并且当前将要执行的命令不允许OOM(一般是写入类命令)
        if (out_of_memory &&
            (c->cmd->flags & CMD_DENYOOM ||
             (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand))) {
            flagTransaction(c);
            // 向客户端返回OOM
            addReply(c, shared.oomerr);
            return C_OK;
        }
    }

    // ...

    /* Exec the command */
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
        call(c,CMD_CALL_FULL);
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }
    return C_OK;

内存释放(淘汰)逻辑

内存的释放主要在freeMemoryIfNeededAndSafe()内进行,如果释放不成功,会返回C_ERRfreeMemoryIfNeededAndSafe()包装了底下的实现函数freeMemoryIfNeeded()

int freeMemoryIfNeeded(void) {
    // slave不管OOM的情况
    if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;

    // ...

    // 获取内存用量状态,如果够用,直接返回ok
    // 如果不够用,这个方法会返回总共用了多少内存mem_reported,至少需要释放多少内存mem_tofree
    // 这个方法很有意思,暗示了其实redis是可以用超内存的。即,在当前这个方法调用后,判断内存足够,但是写入了一个很大的kv,等下一个倒霉蛋来请求的时候发现,内存不够了,这时候才会在下一次请求时触发清理逻辑
    if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
        return C_OK;

    // 用来记录本次调用释放了多少内存的变量
    mem_freed = 0;

    // 不需要evict的策略下,直接跳到释放失败的逻辑
    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
        goto cant_free; /* We need to free memory, but policy forbids. */

    // 循环,尝试释放足够大的内存
    // 同步释放的情况下,如果要删除的对象很多,或者是很大的hash/set/zset等,需要反复循环多次
    // 所以一般在监控里看到有大量key evict的时候,会跟着看到QPS下降,RTT上升
    while (mem_freed < mem_tofree) {
        // 根据配置的maxmemory-policy,拿到一个可以释放掉的bestkey
        // 中间逻辑比较多,可以再开一篇,先略过了
        if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {        // 带LRU/LFU/TTL的策略
            // ...
        }
        else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
                 server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM) {    // 带random的策略
           // ...
        }

        // 最终选中了一个bestkey
        if (bestkey) {
            if (server.lazyfree_lazy_eviction)
                // 如果配置了lazy free,尝试异步删除(不一定异步,相见下文)
                dbAsyncDelete(db,keyobj);
            else
                dbSyncDelete(db,keyobj);

            // ...

            // 如果是异步删除,需要在循环过程中定期评估后台清理线程是否释放了足够的内存,默认每16次循环检查一次
            // 可以想到的是,如果kv都很小,那么前面的操作并不是异步,lazy free不生效。如果kv都很大,那么几乎所有kv都走异步清理,主线程接近空转,如果清理线程不够,那么还是会话相对长的时间的。所以应该是大小混合的场景比较合适lazy free,需要实验数据验证
            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
                if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
                    // 如果释放了足够内存,那么可以直接跳出循环了
                    mem_freed = mem_tofree;
                }
            }
        }
    }

cant_free:
    // 无法释放内存时,做个好人,本次请求卡就卡吧,检查一下后台清理线程是否还有任务正在清理,等他清理出足够内存之后再退出
    while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
        if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
            // 这里有点疑问,如果已经能等到足够的内存被释放,为什么不直接返回C_OK???
            break;
        usleep(1000);
    }
    return C_ERR;
}

异步删除逻辑

// 用来评估是否需要异步删除的阈值
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    // 先从expire字典中删了这个entry(释放expire字典的entry内存,因为后面用不到),不会释放key/value本身内存
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    // 从db的key space中摘掉这个entry,但是不释放entry/key/value的内存
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);

        // 评估要删除的代价
        // 默认1
        // list对象,取其长度
        // 以hash格式存储的set/hash对象,取其元素个数
        // 跳表存储的zset,取跳表长度
        size_t free_effort = lazyfreeGetFreeEffort(val);

        // 如果代价大于阈值,扔给后台线程删除
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }

        // 释放entry内存
    }
}

总结

感觉redis可以考虑一个功能,给一个参数配置内存高水位,超过高水位之后就可以触发evict操作。但是有个问题,可能清理速度赶不上写入速度,怎么合理平衡这两者需要仔细想一下。

另外感叹一下antirez代码层面上的架构能力,几年前看过redis 2.8的代码,从2.8的分支直接切到5.0之后,原来阅读的位置并没有偏离主线太远。历经几个大版本的迭代,加了N多功能之后,代码主体逻辑依旧没有大改,真的是做到了对修改关闭,对扩展开放。向大佬学习

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路

查看原文

赞 1 收藏 0 评论 0

hbprotoss 发布了文章 · 2019-05-18

从实现角度看redis lazy free的使用和注意事项

本文主要从实现角度分析了redis lazy free特性的使用方法和注意事项

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路


众所周知,redis对外提供的服务是由单线程支撑,通过事件(event)驱动各种内部逻辑,比如网络IO、命令处理、过期key处理、超时等逻辑。在执行耗时命令(如范围扫描类的keys, 超大hash下的hgetall等)、瞬时大量key过期/驱逐等情况下,会造成redis的QPS下降,阻塞其他请求。近期就遇到过大容量并且大量key的场景,由于各种原因引发的redis内存耗尽,导致有6位数的key几乎同时被驱逐,短期内redis hang住的情况

耗时命令是客户端行为,服务端不可控,优化余地有限,作者antirez在4.0这个大版本中增加了针对大量key过期/驱逐的lazy free功能,服务端的事情还是可控的,甚至提供了异步删除的命令unlink(前因后果和作者的思路变迁,见作者博客:Lazy Redis is better Redis - <antirez>

lazy free的功能在使用中有几个注意事项(以下为个人观点,有误的地方请评论区交流):

  1. lazy free不是在遇到快OOM的时候直接执行命令,放后台释放内存,而是也需要block一段时间去获得足够的内存来执行命令
  2. lazy free不适合kv的平均大小太小或太大的场景,大小均衡的场景下性价比比较高(当然,可以根据业务场景调整源码里的宏,重新编译一个版本)
  3. redis短期内其实是可以略微超出一点内存上限的,因为前一条命令没检测到内存超标(其实快超了)的情况下,是可以写入一个很大的kv的,当后续命令进来之后会发现内存不够了,交给后续命令执行释放内存操作
  4. 如果业务能预估到可能会有集中的大量key过期,那么最好ttl上加个随机数,匀开来,避免集中expire造成的blocking,这点不管开不开lazy free都一样

具体分析请见下文

参数

redis 4.0新加了4个参数,用来控制这种lazy free的行为

  • lazyfree-lazy-eviction:是否异步驱逐key,当内存达到上限,分配失败后
  • lazyfree-lazy-expire:是否异步进行key过期事件的处理
  • lazyfree-lazy-server-del:del命令是否异步执行删除操作,类似unlink
  • replica-lazy-flush:replica client做全同步的时候,是否异步flush本地db

以上参数默认都是no,按需开启,下面以lazyfree-lazy-eviction为例,看看redis怎么处理lazy free逻辑,其他参数的逻辑类似

源码分析

命令处理逻辑

int processCommand(client *c)是redis处理命令的主方法,在真正执行命令前,会有各种检查,包括对OOM情况下的处理

int processCommand(client *c) {
    // ...

    if (server.maxmemory && !server.lua_timedout) {
        // 设置了maxmemory时,如果有必要,尝试释放内存(evict)
        int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;

        // ...

        // 如果释放内存失败,并且当前将要执行的命令不允许OOM(一般是写入类命令)
        if (out_of_memory &&
            (c->cmd->flags & CMD_DENYOOM ||
             (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand))) {
            flagTransaction(c);
            // 向客户端返回OOM
            addReply(c, shared.oomerr);
            return C_OK;
        }
    }

    // ...

    /* Exec the command */
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
        call(c,CMD_CALL_FULL);
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }
    return C_OK;

内存释放(淘汰)逻辑

内存的释放主要在freeMemoryIfNeededAndSafe()内进行,如果释放不成功,会返回C_ERRfreeMemoryIfNeededAndSafe()包装了底下的实现函数freeMemoryIfNeeded()

int freeMemoryIfNeeded(void) {
    // slave不管OOM的情况
    if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;

    // ...

    // 获取内存用量状态,如果够用,直接返回ok
    // 如果不够用,这个方法会返回总共用了多少内存mem_reported,至少需要释放多少内存mem_tofree
    // 这个方法很有意思,暗示了其实redis是可以用超内存的。即,在当前这个方法调用后,判断内存足够,但是写入了一个很大的kv,等下一个倒霉蛋来请求的时候发现,内存不够了,这时候才会在下一次请求时触发清理逻辑
    if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
        return C_OK;

    // 用来记录本次调用释放了多少内存的变量
    mem_freed = 0;

    // 不需要evict的策略下,直接跳到释放失败的逻辑
    if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
        goto cant_free; /* We need to free memory, but policy forbids. */

    // 循环,尝试释放足够大的内存
    // 同步释放的情况下,如果要删除的对象很多,或者是很大的hash/set/zset等,需要反复循环多次
    // 所以一般在监控里看到有大量key evict的时候,会跟着看到QPS下降,RTT上升
    while (mem_freed < mem_tofree) {
        // 根据配置的maxmemory-policy,拿到一个可以释放掉的bestkey
        // 中间逻辑比较多,可以再开一篇,先略过了
        if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
            server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {        // 带LRU/LFU/TTL的策略
            // ...
        }
        else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
                 server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM) {    // 带random的策略
           // ...
        }

        // 最终选中了一个bestkey
        if (bestkey) {
            if (server.lazyfree_lazy_eviction)
                // 如果配置了lazy free,尝试异步删除(不一定异步,相见下文)
                dbAsyncDelete(db,keyobj);
            else
                dbSyncDelete(db,keyobj);

            // ...

            // 如果是异步删除,需要在循环过程中定期评估后台清理线程是否释放了足够的内存,默认每16次循环检查一次
            // 可以想到的是,如果kv都很小,那么前面的操作并不是异步,lazy free不生效。如果kv都很大,那么几乎所有kv都走异步清理,主线程接近空转,如果清理线程不够,那么还是会话相对长的时间的。所以应该是大小混合的场景比较合适lazy free,需要实验数据验证
            if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
                if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
                    // 如果释放了足够内存,那么可以直接跳出循环了
                    mem_freed = mem_tofree;
                }
            }
        }
    }

cant_free:
    // 无法释放内存时,做个好人,本次请求卡就卡吧,检查一下后台清理线程是否还有任务正在清理,等他清理出足够内存之后再退出
    while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
        if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
            // 这里有点疑问,如果已经能等到足够的内存被释放,为什么不直接返回C_OK???
            break;
        usleep(1000);
    }
    return C_ERR;
}

异步删除逻辑

// 用来评估是否需要异步删除的阈值
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    // 先从expire字典中删了这个entry(释放expire字典的entry内存,因为后面用不到),不会释放key/value本身内存
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    // 从db的key space中摘掉这个entry,但是不释放entry/key/value的内存
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);

        // 评估要删除的代价
        // 默认1
        // list对象,取其长度
        // 以hash格式存储的set/hash对象,取其元素个数
        // 跳表存储的zset,取跳表长度
        size_t free_effort = lazyfreeGetFreeEffort(val);

        // 如果代价大于阈值,扔给后台线程删除
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }

        // 释放entry内存
    }
}

总结

感觉redis可以考虑一个功能,给一个参数配置内存高水位,超过高水位之后就可以触发evict操作。但是有个问题,可能清理速度赶不上写入速度,怎么合理平衡这两者需要仔细想一下。

另外感叹一下antirez代码层面上的架构能力,几年前看过redis 2.8的代码,从2.8的分支直接切到5.0之后,原来阅读的位置并没有偏离主线太远。历经几个大版本的迭代,加了N多功能之后,代码主体逻辑依旧没有大改,真的是做到了对修改关闭,对扩展开放。向大佬学习

有帮助的话就点个赞,关注专栏数据库,不跑路吧~~
不定期更新数据库的小知识和实用经验,让你不用再需要担心跑路

查看原文

赞 1 收藏 0 评论 0

hbprotoss 提出了问题 · 2015-12-15

HttpServletRequestWrapper里的方法没有被调用

我有一系列接口,读写数据都需要加密/解密。写了一个filter,原始request用HttpServletRequestWrapper包装了一下,但是发现HttpServletRequestWrapper的getInputStream和getReader都没有被调用,导致数据到了controller依旧是加密状态,controller报告required parameter is missing

代码

HttpServletRequestWrapper子类

class ResettableStreamHttpServletRequest extends
        HttpServletRequestWrapper {

    private byte[] rawData;
    private HttpServletRequest request;
    private ResettableServletInputStream servletStream;

    public ResettableStreamHttpServletRequest(HttpServletRequest request) {
        super(request);
        this.request = request;
        this.servletStream = new ResettableServletInputStream();
    }


    public void resetInputStream(byte[] data) {
        servletStream.stream = new ByteArrayInputStream(data);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (rawData == null) {
            rawData = IOUtils.toByteArray(this.request.getReader());
            servletStream.stream = new ByteArrayInputStream(rawData);
        }
        return servletStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (rawData == null) {
            rawData = IOUtils.toByteArray(this.request.getReader());
            servletStream.stream = new ByteArrayInputStream(rawData);
        }
        return new BufferedReader(new InputStreamReader(servletStream));
    }


    private class ResettableServletInputStream extends ServletInputStream {

        private InputStream stream;

        @Override
        public int read() throws IOException {
            return stream.read();
        }
    }
}

doFilter代码

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest newRequest = new ResettableStreamHttpServletRequest((HttpServletRequest) request);
        ServletResponse newResponse = new EncryptedResponseWrapper((HttpServletResponse) response);

        String body = IOUtils.toString(newRequest.getInputStream());
        String plainText = crypt.decrypt(body);
        LOGGER.debug(plainText);
        ((ResettableStreamHttpServletRequest) newRequest).resetInputStream(plainText.getBytes("UTF-8"));

        chain.doFilter(newRequest, newResponse);

        if (((EncryptedResponseWrapper) newResponse).getStatus() != HttpStatus.OK.value()) {
            response.getWriter().write(newResponse.toString());
            return;
        }

        String text = newResponse.toString();
        if (text != null) {
            String respPlainText = newResponse.toString();
            LOGGER.debug(respPlainText);
            String encrypted = crypt.encrypt(respPlainText);
            response.getWriter().write(encrypted);
        }
    }

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <absolute-ordering>
        <name>EncryptFilter</name>
        <name>encodingFilter</name>
    </absolute-ordering>

    <filter>
        <filter-name>EncryptFilter</filter-name>
        <filter-class>com.yuexunit.micro.filter.EncryptFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>EncryptFilter</filter-name>
        <url-pattern>/user/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

关注 2 回答 1

hbprotoss 赞了回答 · 2015-12-15

select count(1) 统计太慢,怎么优化?

你的表按年进行分区,查询时间条件也是一整年,这样虽然走了分区但是这一个分区的数据还是比较多.
你的查询业务需要有这么大的跨度吗? 是否按照你实际的查询跨度来优化一下分区方式?
如果确实有大量数据而且是存量数据,可以考虑直接把一些维度统计好放在数据库,后期查询直接查询结果,不再动态统计.

关注 8 回答 1

hbprotoss 回答了问题 · 2015-10-21

解决求一个URL匹配的正则表达式

我来回答一下自己N年前的问题。。。

Python支持negative lookbehind assertion

In [9]: re.search(r'.*(?<!google)\.cn.*', "www.baidu.cn").group(0)
Out[9]: 'www.baidu.cn'

In [14]: re.search(r'.*(?<!google)\.cn.*', "www.google.cn")
Out[14]: None

关注 2 回答 4

hbprotoss 回答了问题 · 2015-07-31

关于python 切片的问题

Python-2.7.8/objects/listobject.c +687

c    else if (d > 0) { /* Insert d items */
        k = Py_SIZE(a);
        if (list_resize(a, k+d) < 0)
            goto Error;
        item = a->ob_item;
        memmove(&item[ihigh+d], &item[ihigh],
            (k - ihigh)*sizeof(PyObject *));
    }
    for (k = 0; k < n; k++, ilow++) {
        PyObject *w = vitem[k];
        Py_XINCREF(w);
        item[ilow] = w;
    }

关注 7 回答 4

hbprotoss 评论了回答 · 2013-06-30

Sublime text有没有会话管理的插件?

认证与成就

  • 获得 11 次点赞
  • 获得 19 枚徽章 获得 1 枚金徽章, 获得 9 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2012-09-11
个人主页被 476 人浏览