Redis常用数据结构以及多并发场景下的使用分析:list类型
文章目录
- 前言
- redis 中的list结构
- 为什么使用 ziplist
- 为什么使用linkedlist
- quicklist = ziplist+linkedlist
- 使用场景
- 维护最近访问记录
- 简单的消息队列
- 总结
前言
首先我们学习到了 String 和Hash 两种数据类型 首先兄弟们先去复习下这两种结构哟~
Redis常用数据结构以及多并发场景下的使用分析:String类型
Redis常用数据结构以及多并发场景下的使用分析:Hash类型
本文将去探索 redis内部的list的实现原理 以及基于此数据结构 有关于list的应用
首先我们先去看看 redis 内部的list是如何去维护的 还挺有趣的这部分
redis 中的list结构
Redis版本 | List底层结构 |
---|---|
Redis 2.x 及之前 | ziplist(压缩列表) + linkedlist(双向链表) |
Redis 3.2 起 | 引入 quicklist,替代上述两种 |
Redis 5.x 后 | 默认 List 使用 quicklist,不再使用 linkedlist/ziplist 单独实现 |
为什么使用 ziplist
ziplist 是什么?
是一种连续内存块组成的紧凑结构
每个元素按顺序排列在一块连续内存中 节省空间,
适合少量元素的小型列表
很好理解 redis是在内存中工作的 而为了 最大效率的实现内存中操作的效率 那此时可以想到利用【cache】的原理 去加速访问数据
将数据连续的存储 放在一个连续的内存地址中 那么就可以大大的利用上内存中的缓存原理
就像数组 arr[0], arr[1], arr[2] 在一块内存里,L1/L2 Cache 一次可以加载多个元素 → 内存局部性好。
为什么使用linkedlist
ziplist 有什么缺点?
删除操作耗时 时间复杂度为O(n) 因为需要将后面的元素全部移动到删除节点
如何提高 插入删除速度 -》基于链表的队列
linkedlist 是什么? Redis 早期使用的 List 结构
每个节点是一个对象,包含前指针、后指针、value
其实就是通用意义上的linkedList 相比于ziplist 具有 插入删除快的性质 【O(1),只修改指针】
quicklist = ziplist+linkedlist
ok 你了解了ziplist 也了解了linkedlist 那么你就能了解到为什么 需要将两个结构结合起来 使用了
ziplist 能利用缓存的局部性 内存占用低 内存紧凑
linkedlist 插入删除操作快 内存占用高 内存破碎
那么就讲这两个结构结合起来 就变成了edis 5.x 后 默认的结构 quicklist
quicklist 是 ziplist + linkedlist 的融合产物 是 Redis List 的默认实现结构
quicklist 是什么?
是 多个 ziplist 的组合,用 双向链表连接起来
每个节点是一个 ziplist
整个链表类似于:“ziplist1 <-> ziplist2 <-> ziplist3…”
图示如下:
[quicklist]|├── [ziplist node 1] → [ziplist node 2] → [ziplist node 3] ...
插入的具体分析:
例子:LPUSH mylist A(从左边插入)
定位到最左边的 ziplist 节点(quicklist 的 head)如果该 ziplist 未超过最大容量(list-max-ziplist-size),则直接插入到该 ziplist 的头部否则:创建一个新的 ziplist 节点将该新 ziplist 插入到链表头部插入数据到新 ziplist
RPUSH mylist B(从右边插入)
逻辑一样,只不过是定位到尾部 ziplist
示例流程:假设 max-ziplist-size = 3,执行如下命令:
LPUSH mylist A
LPUSH mylist B
LPUSH mylist C
LPUSH mylist D
这就是插入的最终结果
quicklist:↓ head ↓ tail
[ziplist-1] ←→ [ziplist-2]ziplist-1: [D C B]
ziplist-2: [A]
访问顺序仍是:
D → C → B → A
但是需要注意 quicklist 由于还是使用的多个ziplist 组 去进行连接 所以对于 基于索引的插入 还是还是 O(n)的时间复杂度哟
想想为什么?
底层是 quicklist:
虽然是链表 + ziplist,但 不支持快速定位某个索引
要插入前,Redis 需要从头开始遍历整个 quicklist 和每个 ziplist
时间复杂度:O(n),而不是 O(1)
quicklist结合了两者的优点
特性 | 说明 |
---|---|
综合了两者优点 | ziplist 节省空间 + linkedlist 快速插入删除 |
快速定位 | 支持头尾插入、遍历,性能好 |
节点压缩 | 可以配置压缩多个节点,减少内存 |
灵活性强 | 可调参数:每个 ziplist 的元素个数、压缩策略等 |
使用场景
在讲解使用场景之前 首先问你一个问题 基于以上的数据结构 你觉得可以去完成什么事情。
其实可以把它当作一个
一个全局共享的(存放在redis中)、有序可变链表结构(quicklist),支持线程安全(redis 单线程特性决定的)的头尾插入/弹出操作 的数组(头尾有指针去维护)。
你去这样想 就能够理解redis中的list到底是什么了 其实数组能做的 它都能应用
维护最近访问记录
@Service
@RequiredArgsConstructor
public class RecentVisitService {private final RedisTemplate<String, String> redisTemplate;// 最大保留记录数private static final int MAX_RECENT_COUNT = 10;/*** 添加用户最近访问记录(文章ID)* @param userId 用户ID* @param articleId 文章ID*/public void addRecentVisit(String userId, String articleId) {String key = "user:" + userId + ":recent:articles";// 插入到列表头部redisTemplate.opsForList().leftPush(key, articleId);// 截断:保留前 10 个redisTemplate.opsForList().trim(key, 0, MAX_RECENT_COUNT - 1);}/*** 获取最近访问记录* @param userId 用户ID* @return 最近访问的文章ID列表(按访问时间倒序)*/public List<String> getRecentVisits(String userId) {String key = "user:" + userId + ":recent:articles";return redisTemplate.opsForList().range(key, 0, MAX_RECENT_COUNT - 1);}
}
测试类
@Testpublic void testAddAndGetRecentVisit() {for (int i = 1; i <= 12; i++) {recentVisitService.addRecentVisit(userId, "article-" + i);}List<String> recentArticles = recentVisitService.getRecentVisits(userId);// 断言最多保留10条assertEquals(10, recentArticles.size());// 断言最近的是 article-12assertEquals("article-12", recentArticles.get(0));// 断言最后一条是 article-3(前3被裁剪掉)assertEquals("article-3", recentArticles.get(9));}
结果
简单的消息队列
生产者 LPUSH 进入队列
消费者 RPOP 消费队列元素
LPUSH queue:order order123 // 原子性插入
RPOP queue:order // 原子性弹出
LLEN queue:order // 原子性获取长度// 支持多生产者多消费者并发访问
生产者A: LPUSH queue:payment payment1
生产者B: LPUSH queue:payment payment2
消费者C: RPOP queue:payment → payment1
消费者D: RPOP queue:payment → payment2
总结
Redis List在高并发场景的核心价值:
极低延迟:内存操作,毫秒级响应
高吞吐量:单机可达10万+QPS
简单可靠:操作原子性,代码简洁
实时性强:适合需要即时处理的场景
成本低:无需额外的MQ组件
适用场景:
实时消息通知
用户动态时间线
简单的任务队列
最近访问记录
实时日志收集
学到了list实现原理理解上面的场景都比较容易