Advertisement

Redis通信协议之RESP协议

阅读量:

Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub):

客户端(client)向服务端(server)发送一条命令

服务端解析并执行命令,返回响应结果给客户端

基于此,在客户端发送指令的方式与服务端返回的数据形式之间必须遵循一定的规范;而这种规范即为通信协议的标准。

而在Redis中采用的是RESP(Redis Serialization Protocol)协议:

Redis 1.2版本引入了RESP协议

Redis 2.0版本中成为与Redis服务端通信的标准,称为RESP2

在Redis 6.0版本中采用了 RESP3 协议替代了 RESP2,并新增了更多数据类型以兼容6.0新功能。

然而目前我们仍使用的是RESP2协议(简称RESP)。这也是我们需要掌握的基础知识。

响应头中包含关键信息以区分不同数据类型,在HTTP/1.1协议中定义了五种常用的响应体数据类型

首字符为 ' + ' ,随后紧跟一段独立的字符串内容,并以CRLF编码方式结束。例如,在返回 ' OK ' 时会生成 '+ OK \r\n'

错误(Error records):每个字符串的第一字符为负号‘-’;其形式类似于单行字符串格式,在此情况下仅表示异常内容,请参考示例 '-Error message\r\n'

数值:首字节是 ‘:’ ,后面跟上数字格式的字符串,以CRLF结尾。例如:":10\r\n"

多行字符串:首字节是 ‘$’ ,表示二进制安全的字符串,最大支持512MB:

如果大小为0,则代表空字符串:"$0\r\n\r\n"

如果大小为-1,则代表不存在:"$-1\r\n"

数组:首字节是 ‘*’,后面跟上数组元素个数,再跟上元素,元素数据类型不限:

Redis通信协议-基于Socket自定义Redis的客户端

Redis基于TCP协议实现了数据传输功能;为了模拟客户端的行为模式,我们可以通过Socket实现客户端与服务器之间的通信

复制代码
  
    
     static Socket s;
    
     static PrintWriter writer;
    
     static BufferedReader reader;
    
  
    
     public static void main(String[] args) {
    
     try {
    
         // 1.建立连接
    
         String host = "192.168.150.101";
    
         int port = 6379;
    
         s = new Socket(host, port);
    
         // 2.获取输出流、输入流
    
         writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
    
         reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
    
  
    
         // 3.发出请求
    
         // 3.1.获取授权 auth 123321
    
         sendRequest("auth", "123321");
    
         Object obj = handleResponse();
    
         System.out.println("obj = " + obj);
    
  
    
         // 3.2.set name 虎哥
    
         sendRequest("set", "name", "虎哥");
    
         // 4.解析响应
    
         obj = handleResponse();
    
         System.out.println("obj = " + obj);
    
  
    
         // 3.2.set name 虎哥
    
         sendRequest("get", "name");
    
         // 4.解析响应
    
         obj = handleResponse();
    
         System.out.println("obj = " + obj);
    
  
    
         // 3.2.set name 虎哥
    
         sendRequest("mget", "name", "num", "msg");
    
         // 4.解析响应
    
         obj = handleResponse();
    
         System.out.println("obj = " + obj);
    
     } catch (IOException e) {
    
         e.printStackTrace();
    
     } finally {
    
         // 5.释放连接
    
         try {
    
             if (reader != null) reader.close();
    
             if (writer != null) writer.close();
    
             if (s != null) s.close();
    
         } catch (IOException e) {
    
             e.printStackTrace();
    
         }
    
     }
    
     }
    
  
    
     private static Object handleResponse() throws IOException {
    
     // 读取首字节
    
     int prefix = reader.read();
    
     // 判断数据类型标示
    
     switch (prefix) {
    
         case '+': // 单行字符串,直接读一行
    
             return reader.readLine();
    
         case '-': // 异常,也读一行
    
             throw new RuntimeException(reader.readLine());
    
         case ':': // 数字
    
             return Long.parseLong(reader.readLine());
    
         case '$': // 多行字符串
    
             // 先读长度
    
             int len = Integer.parseInt(reader.readLine());
    
             if (len == -1) {
    
                 return null;
    
             }
    
             if (len == 0) {
    
                 return "";
    
             }
    
             // 再读数据,读len个字节。我们假设没有特殊字符,所以读一行(简化)
    
             return reader.readLine();
    
         case '*':
    
             return readBulkString();
    
         default:
    
             throw new RuntimeException("错误的数据格式!");
    
     }
    
     }
    
  
    
     private static Object readBulkString() throws IOException {
    
     // 获取数组大小
    
     int len = Integer.parseInt(reader.readLine());
    
     if (len <= 0) {
    
         return null;
    
     }
    
     // 定义集合,接收多个元素
    
     List<Object> list = new ArrayList<>(len);
    
     // 遍历,依次读取每个元素
    
     for (int i = 0; i < len; i++) {
    
         list.add(handleResponse());
    
     }
    
     return list;
    
     }
    
  
    
     // set name 虎哥
    
     private static void sendRequest(String ... args) {
    
     writer.println("*" + args.length);
    
     for (String arg : args) {
    
         writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
    
         writer.println(arg);
    
     }
    
     writer.flush();
    
     }
    
 }

Redis内存回收-过期key处理

Redis的卓越性能归因于以内存存储为核心的技术架构设计。然而,在单节点架构下,Redis的最佳内存量不宜过大;否则可能会导致数据持久化或主从同步功能受到影响。我们可以通过修改配置文件来设置Redis的最大内存:

一旦内存使用接近上限时,则会阻止新增数据。为了应对这一问题,Redis引入了几种策略以实现有效的内存回收机制:

内存过期策略

在学习Redis缓存的时候我们提到过,在我们的讨论中提到了使用expire命令来设置Redis key的TTL值为指定的 lifetime。

观察到当某个变量的过期时间到达时,在访问其名称时会返回nil值表明该变量已不再存在;相应的内存空间被回收从而实现了整体的内存回收策略。

Redis是一种典型的键值型内存数据库,在其中的所有键值都储存在之前学习过的字典(Dict)结构中。在其数据库(database)结构体内包含有多个字典(Dict),其中一个用于存储键-值对(key-value),另一个用于存储键-过期时间对(key-TTL)。

这里有两个问题需要我们思考: Redis是如何知道一个key是否过期呢?

利用两个Dict分别记录key-value对及key-ttl对

是不是TTL到期就立即删除了呢?

惰性删除

惰性删除:顾明思议并非是在TTL(Time To Live)过期后立即执行删除操作,在访问某个特定的键时会先对其实存情况进行评估与判断;一旦发现该键已失效就进行相应的删除操作

周期删除

该机制采用定期启动一个定时脚本的方式,在每个预设时间段内检查并移除已过期的关键字项。其运行模式分为两种类型:一种是Redis服务在初始化阶段自动配置并启动一个基于server.hz参数设置频率的定时任务;另一种则是直接在Redis事件循环开始前触发beforeSleep()函数以完成清理操作(SLOW)。

SLOW模式规则:

执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。

执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms

逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期

如果未达到时间限制(25ms)且已过期的键的比例超过10%,则会再次抽取样本;否则将不再继续。

FAST模式规则(过期key比例小于10%不执行 ):

执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms

执行清理耗时不超过1ms

依次遍历数据库中的每一个桶及其内容,请从每个桶中随机抽取20个键值对进行时间验证。若当前的时间限制未满(最多1毫秒)且已过期键的比例超过10%,则需再次从该桶中随机抽取样本进行检查。

小总结

RedisKey的TTL记录方式:

在RedisDB中通过一个Dict记录每个Key的TTL时间

过期key的删除策略:

惰性清理:每次查找key时判断是否过期,如果过期则删除

定期执行清理操作:按照固定周期对系统中的某些键进行检查与评估其是否已失效。若发现已失效,则将其移除。 定期清理可采用以下两种主要方式:

SLOW模式执行频率默认为10,每次不超过25ms

FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

Redis内存回收-内存淘汰策略

内存管理策略:即当Redis内部占用接近预先设定的最大内存容量时,系统会主动移除部分键值对以优化空间利用率。Redis在处理客户端命令的过程中,在方法processCommand()中会实施这一策略。

淘汰策略

Redis支持8种不同策略来选择要删除的key:

noeviction: 不会淘汰任何键,在内存满载时将无法新增数据,默认情况下采用此策略。

该机制通过比较具有 TNL 设置的键的剩余有效时间( TTL )值来决定哪些键会被优先淘汰

allkeys-random:通过全体key 随机淘汰的方式来实现。等同于直接从db->dict中进行随机选择。

volatile-random:设置有时间限制的键会随机删除这些条目,并对应地从db->expires中进行随机选择。

allkeys-lru: 对全体key,基于LRU算法进行淘汰

volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

allkeys-lfu: 对全体key,基于LFU算法进行淘汰

volatile-lfu:对于那些设置了TTL值的键,在应用LFI算法策略时会进行移除操作。两个容易引起混淆的情况分别是:

LRU(Least Recently Used),即"最近使用频率"。通过计算自上一次访问以来的时间间隔来确定页面的存废顺序:其数值越大,则该页面的访问优先权将被提升得更高。

LFU(Lowest Frequency Utilization),最低频使用。计算每个key被访问的次数,并将那些访问次数较少的key会被优先淘汰。

Redis的数据都会被封装为RedisObject结构:

LFU的访问次数被称为逻辑访问次数的原因在于,并非每次key被访问都会进行计数操作;相反地,在某些情况下会通过运算来确定是否需要更新。

生成0~1之间的随机数R

计算 (旧次数 * lfu_log_factor + 1),记录为P

如果 R < P ,则计数器 + 1,且最大不超过255

访问次数随着时间呈衰减趋势变化;间隔上一次访问的时间长度每次增加 lfu_decay_time 分钟;计数器值每次减少 1。

最后用一副图来描述当前的这个流程吧

全部评论 (0)

还没有任何评论哟~