一、info指令(TODO)
二、再谈分布式锁
场景:在集群环境下,前文的分布式锁存在缺陷。在Sentinel集群中,当主节点挂掉,从节点会取而代之,若原先第一个客户端在主节点成功申请了一把锁,但加锁命令还未来得及同步给从节点,主节点挂了,从节点升级为主节点。此时的主节点并没有锁信息,若有新的客户端请求加锁,也会成功,一把锁就会被多个客户端持有。
1. RedLock算法
使用条件:为了使用redlock,需要使用多个redis实例,实例之间相互独立,没有主从关系
原理:redlock和大多数分布式系统类似,使用“大多数机制”:
- 加锁时,他会向过半节点发送 set(key, value ,nx=True,ex=XXX) 的请求,只要过半节点set成功,则认为加锁成功
- 释放锁时,需要向所有节点发送del请求
- redlock算法还需要考虑出错重试,时钟漂移等问题
2. RedLock使用场景
场景:如果挂了一台redis也不完全受影响,则应该考虑redlock算法;由于需要多台redis实例,性能下降了,使用时需要斟酌
三、过期策略
1. 过期的key集合
过期key集合:redis会将每个设置了过期时间的key都加入一个独立的字典中,以后会定时遍历字典,删除过期key
惰性删除策略:客户端在访问key时,会判断key是否过期,过期则删除
2. 定时扫描策略
步骤:redis默认每秒会进行10次过期扫描,过期扫描不会遍历过期字典中所有key,而会采用一种贪心策略
- 从过期字典中随机选取20个key
- 删除这20个key中已经过期的key
- 若过期的key占比超过1/4,重复步骤1;若定期扫描流程运行时长超过25ms,退出循环
例子-大批量key同时过期:
问题:
- 若一个大型redis实例中所有的key过期了,redis会持续扫描过期字典,直到字典过期key变稀疏才会停止,进而导致请求卡顿(卡顿也是因为频繁回收内存页导致的)
- 若客户端请求的超时时间设置得比较短,小于25ms,而redis服务器正好在进行定时扫描,此时客户端连接就会出现超时关闭的情况
解决方法:
- 如果有大批量key过期,可以给过期时间设置一个随机范围
redis.expire_at(key, random.randint(time) + expire_ts)
3. 从节点的过期策略
流程:从节点不会进行过期扫描,主节点的key过期后,会在AOF文件上增加一条del指令,同步到所有从节点,从节点执行指令删除过期key
问题:指令同步是异步进行的,若主节点过期的key的del指令没有及时同步到从节点,就会出现主从不一致的问题(分布式锁可能因为这种情况导致加锁失败)
四、LRU
1. 概述
场景:若开启swap,redis内存超出限制,内存数据频繁与磁盘交互,会让redis的性能急剧下降。生产环境中,是不允许redis出现交换行为的,为了限制最大使用内存,redis提供了maxmemory参数限制内存。
内存清理策略:当实际内存超出maxmemory时,redis提供了几种可以选择的策略:
- noeviction:不会继续接收写请求,接收读请求(默认策略)
- volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先淘汰,没有设置过期时间的key不会被淘汰
- volatile-ttl:尝试淘汰设置了过期时间的key,优先淘汰剩余寿命最短的key
- volatile-random:尝试淘汰设置了过期时间的key,淘汰策略为随机选择key淘汰
- allkeys-lru:使用lru淘汰key,候选集为所有key
- allkeys-random:随机淘汰key,候选集为所有key
2. LRU算法
实现思路见操作系统,虚拟内存部分,以下为代码实现:
package redis.LRU;
import opLearn.utils.LinkList;
public class LRUTest<T> {
static class Node<T> {
T data;
Node<T> next;
}
private Node<T> head;
private Node<T> tail;
private int size;
private int cnt;
public LRUTest(int size) {
head = new Node<>();
tail = head;
this.size = size;
this.cnt = 0;
}
public T addData(T data) {
this.cnt++;
T res = null;
if (this.cnt > this.size) {
res = delHead();
}
Node<T> node = new Node<T>();
node.data = data;
tail.next = node;
tail = node;
return res;
}
public T delHead() {
T res = null;
if (this.cnt > 0) {
this.cnt--;
Node<T> point = head.next;
res = point.data;
head.next = point.next;
head = point;
}
return res;
}
public T search(T data) {
Node<T> point = head.next;
Node<T> prePoint = head;
while (point != null && point.data != data) {
prePoint = point;
point = point.next;
}
if (point == null) {
return null;
}
Node<T> tempNode = point;
// 删除point
prePoint.next = point.next;
// 加到队列尾部
tail.next = tempNode;
tail = tempNode;
tail.next = null;
return tempNode.data;
}
public void prinf() {
Node<T> point = head.next;
while (point != null) {
System.out.printf("%s\t", point.data.toString());
point = point.next;
}
System.out.println();
}
public static void main(String[] args) {
LRUTest<String> linkList = new LRUTest(4);
linkList.addData("a");
linkList.addData("b");
linkList.addData("c");
linkList.addData("d");
String[] pageUse = {"c", "a", "d", "b", "e", "b", "a", "b", "c", "d"};
for (String page : pageUse) {
String search = linkList.search(page);
String out = null;
if (search == null) {
out = linkList.addData(page);
}
System.out.printf("淘汰:%s 插入:%s\n", out, page);
linkList.prinf();
System.out.println("==================");
}
}
}
3. 近似LRU算法
LRU算法问题:LRU算法会消耗额外的内存,且每次访问时需要遍历链表,时间复杂度较高
redis的近似LRU算法:
- 给每个key增加24bit的额外字段,记录最后一次被访问的时间戳
- 当redis执行写操作时,发现内存超过maxmemory,就会执行一次LRU淘汰算法(惰性删除)
- 淘汰算法随机从候选池中选择5个key(maxmemory_samples配置),然后淘汰最旧的,若淘汰后还是超过了,继续随机采样,直到内存低于maxmemory
- 淘汰池是一个数组,大小可以是maxmemory_samples,每一次淘汰循环中,新一轮随机得到的key列表会和淘汰池的key融合,淘汰掉最旧的key后,会保留最旧的maxmemory_samples个key,进入下次循环
与LRU算法的效果对比图如下:
五、懒惰删除
redis内部实现并不止一个主线程,还有几个异步线程处理一些耗时的操作
1. redis为什么使用懒惰删除
a. 概述
场景:删除指令del会直接释放对象的内存,若key很大,删除操作会导致单线程卡顿。为了解决卡顿问题,redis4.0引入了unlink指令,对删除操作进行懒处理,丢给后台线程来异步回收内存
unlink并发问题:unlink指令发出后,会将连接指针删除,后续请求无法访问数据,不存在并发问题
flush异步指令:flushdb和flushall指令执行缓慢,redis4.0引入了异步化操作,在指令后加async参数即可
b. 懒惰删除原理
流程:
- 主线程在将待删除对象的引用删除后,会将这个key的内存回收操作包装成一个任务,塞进异步队列中。后台线程会从异步队列中取任务
- 异步队列被主线程和后台线程同时操作,因此必须是线程安全的
- 不是所有的key在unlink后都会延后处理,若对应的key占用的内存很小,redis会将对应的key立即回收,和del指令一样
c. AOF异步操作
场景:redis默认会每1秒执行一次AOF同步命令,调用sync函数,该操作耗时比较大,会导致主线程效率下降,因此redis将该操作移到异步线程来完成
原理:AOF sync操作的线程是一个独立的异步线程,与懒惰删除线程不是一个,它也有一个专属的任务队列,用以存放AOF sync任务
5. 更多异步删除点
删除点:除了del和flush指令外,redis在key的过期、LRU淘汰、rename指令过程中,也会进行回收内存操作;还有一种特殊的flush操作,发生于全量同步的从节点中,在接收完整的rdb后,会对当前内存进行一次性清空
删除点配置:
- slave-lazy-flush:从节点接收rdb文件后的flush操作
- lazyfree-lazy-eviction:内存达到maxmemory是进行淘汰
- lazyfree-lazy-expire key:过期删除
- lazyfree-lazy-server-del rename:指令删除 destKey
六、优雅的使用Jedis(TODO)
七、保护redis
1. 指令安全
场景:redis有部分指令影响实例稳定,导致数据安全问题等。例如keys指令会导致卡顿,flushdb和flushall会导致数据被清空
解决方法:
redis提供了rename-command指令将某些危险指令改名,避免人为操作错误。如果想完全封杀某条指令,可以将指令rename成空串
rename-command keys abckeysabd
rename-command flushall ""
2. 端口安全
场景:redis默认会监听6379端口,若当前服务器主机可以被外网访问,redis会直接暴露在公网上。redis服务地址一旦可以被外网访问,内部数据就失去了安全性
解决方法:
- redis配置文件中必须指定监听的redis地址
- redis需要增加密码访问限制。密码访问限制会影响从节点复制,从节点需要在配置文件中使用masterauth参数指定相应密码
3. Lua脚本安全
解决方法:
- 开发者必须禁止Lua脚本由用户输入的内容生成,避免被植入恶意攻击代码获取redis的主机权限
- 同时,应该让redis使用普通用户身份登录,这样即使存在恶意代码,黑客也无法拿到root权限
4. SSL代理
场景:redis并不支持SSL连接,这意味着客户端和服务器之间交互的数据不应该直接暴露在公网传输上,否则会有被监听的风险。如果必须暴露在公网上,可以考虑使用SSL代理。以下为使用spiped对ssh通道二次加密示意图
ssh代理也可以用在主从复制上,如果redis主从实例需要跨机房复制,spiped也可以派上用场
八、redis安全通讯
1. 概述
场景:假设应用程序部署于A机房,存储部署于B机房,若使用tcp传输数据,数据暴露于公网,客户端与服务器交互的数据存在被窃听的风险
解决方法:redis本身并不支持SSL安全链接,不过可以使用SSL代理软件,让通信数据得到加密
2. spiped原理
spiped处理流程:
- spiped会在客户端和服务器端各自启动一个进程
- 客户端的spiped进程负责接收来自redis client发送过来的请求数据,加密后传送到右边的spiped进程
- 服务器端的spiped进程将接受到的数据解密后,传递到redis server,然后redis server通过反向流程将数据响应回复给客户端