在 Redis 核心数据结构(三) 中,重点介绍了一下 Redis 7+ 使用的底层的数据结构 listpack。本文重点看一下,Redis 是如何基于 listpack 以及其他数据结构类型来构建对外暴露的五个核心数据结构的。
quicklist 关于 quicklist 更详细的介绍,请看 Redis 核心数据结构(一:quicklist。
与上述内容不一样的地方是,现在的 quicklist 底层是使用 listpack 来构建的,而不是上述内容介绍的 ziplist。
list 关于 list-max-listpack-size 的解释,在源码中找到了详细介绍:
redis.conf # Lists are also encoded in a special way to save a lot of space. # The number of entries allowed per internal list node can be specified # as a fixed maximum size or a maximum number of elements. # For a fixed maximum size, use -5 through -1, meaning: # -5: max size: 64 Kb <-- not recommended for normal workloads # -4: max size: 32 Kb <-- not recommended # -3: max size: 16 Kb <-- probably not recommended # -2: max size: 8 Kb <-- good # -1: max size: 4 Kb <-- good # Positive numbers mean store up to _exactly_ that number of elements # per list node. # The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size), # but if your use case is unique, adjust the settings as necessary. list-max-listpack-size -2
在五年前,D瓜哥写了 Redis 核心数据结构(一) 和 Redis 核心数据结构(二) 两篇文章,来对 Redis 内部的数据结构做了深入分析。随着时间的推移,Redis 的实现也在不断进化,现在这些内容已经跟不上最新发展了,推陈出新,现在重写文章,来介绍 Redis 的最新发展。
listpack 从 Redis 7.0 开始,使用 listpack 替换原来的 ziplist。至于替换原因,在 [NEW] listpack migration - replace all usage of ziplist with listpack 做了解释说明:
The reason for using listpack instead of ziplist is that ziplist may cause cascading updates when insert and delete in middle, which is the biggest problem.
— sundb 翻译过来:当在中间进行插入和删除时,ziplist 也许会产生级联更新,这是一个大问题。
编码规范 图 1. listpack 编码格式 相比 ziplist,listpack 更偏向空间换时间。淡化极致的内存使用率,向更快的方向发力。
对整数编码
在一个项目开量验证过程中,发现 createDate 字段不正确,比正确时间晚了十四个小时。调研发现,这是一个非常典型的问题。现在把定位问题的思路和解决办法给大家做个分享。
首先,检查数据库配置,查询线上生产环境配置,结果如下:
图 1. MySQL 变量 同时,检查线上生产环境 MySQL 版本,为问题复现做准备:
图 2. MySQL 版本 从数据库配置上来说,基本正常,没有发现什么问题。(持续运行了这么长时间,有问题应该早就发现了。)
其次,检查数据库连接配置,正式环境的链接配置如下:
jdbc:mysql://<host>:3306/<schema>?createDatabaseIfNotExist=true &characterEncoding=utf-8&useUnicode=true&connectTimeout=2000 &socketTimeout=2000&autoReconnect=true 数据库连接也没有问题。
第三,询问 SA 线上服务器时区配置,回复上是 CST,这个和数据库对应,没有问题。
图 3. 与 SA 沟通 配置检查正常,那么只好在本地搭建环境,重现问题,再寻求解决方案。由于项目是基于 Spring Boot 2.3.7.RELEASE 开发的,相关依赖也尽量使用 Spring Boot 指定版本的,所以,很快把开发环境搭好了。
在配置服务器环境时,遇到一点小小的问题:我一直以为有个时区名称叫 CST,就在网上去查怎么设置,结果徒劳半天也没有找到。后来上开发机检查开发机时区配置,发现是 Asia/Shanghai。将测试服务器设置为该时区,数据库内部查询时区,显示和服务器一直。
调试代码中,发现 MySQL 连接驱动的代码中,有配置时区的相关代码,如下:
com.mysql.cj.protocol.a.NativeProtocol#configureTimezone /** * Configures the client's timezone if required. * * @throws CJException * if the timezone the server is configured to use can't be * mapped to a Java timezone. */ public void configureTimezone() { // 获取服务器时区 String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone"); // 如果服务器时区是 SYSTEM,则使用服务器的 system_time_zone 时区设置 if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) { configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone"); } // 获取客户端时区配置 String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue(); // 如果服务器时区不为空,切客户端时区配置不可用,则使用服务器的时区配置 if (configuredTimeZoneOnServer != null) { // user can override this with driver properties, so don't detect if that's the case if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) { try { canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor()); } catch (IllegalArgumentException iae) { throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor()); } } } if (canonicalTimezone != null && canonicalTimezone.length() > 0) { // 为该会话设置时区 this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone)); // // The Calendar class has the behavior of mapping unknown timezones to 'GMT' instead of throwing an exception, so we must check for this... // if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }), getExceptionInterceptor()); } } }
本文内容对于 Redis 7+ 来说已经过时,最新实现请看下面两篇文章:
Redis 核心数据结构(3)
Redis 核心数据结构(4)
Redis 目前是使用最广泛的缓存中间件。其突出特点就是支持多种常见的数据结构。对比 JDK 集合类的实现,Redis 的实现表现出很多独到之处,很多地方设计得别具匠心。下面就来简要介绍一下。
linkedlist Redis 底层也有很多地方使用到 linkedlist,并且也是双向链表。
adlist.h typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; typedef struct listIter { listNode *next; int direction; } listIter; typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned long len; } list; Redis 的 linkedlist 实现特点是:
双向:节点带有前后指针;
无环:首尾没有相连,所以没有构成环状;
链表保存了首尾指针;
多态:可以保存不同类型的值,这里成为泛型也许更符合 Java 中的语义。
Redis 在 2014 年实现了 quicklist,并使用 quicklist 代替了 linkedlist。所以,现在 linkedlist 几乎已经是废弃状态。
ziplist Redis 官方在 ziplist.c 文件的注释中对 ziplist 进行了定义:
The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist.