Redis
数据结构:
字符串(String)
- 应用场景:适用于简单的键值对存储,如缓存网页内容、计数器等。
- 底层实现:基于简单动态字符串(SDS, Simple Dynamic String)。SDS允许高效地追加和修改字符串,并且在内部维护了长度信息,使得获取字符串长度的操作时间复杂度为O(1)。
哈希表(Hash)
- 应用场景:用于存储对象,例如用户信息(包含字段如姓名、年龄等),或作为小型数据库表的映射。
- 底层实现:使用字典(hash table)实现。对于小数量的键值对,可能会使用ziplist(压缩列表)来节省空间;当元素数量超过配置阈值时,会转换为更通用但可能占用更多内存的hash table表示形式。
列表(List)
- 应用场景:适用于构建队列、栈,或是需要保持插入顺序的数据集合,如评论系统中的最新评论列表。
- 底层实现:可以使用双端链表(linked list)或者ziplist。对于较小的列表,Redis倾向于使用ziplist以减少内存消耗;随着列表的增长,它将转换为更高效的双端链表形式,以便于快速地从两端进行插入和删除操作。
集合(Set)
- 应用场景:适合用于保证数据唯一性、成员关系测试以及执行集合运算(如并集、交集和差集)的场景,比如社交网络中的好友推荐功能。
- 底层实现:通常使用整数集合(intset)或哈希表(hash table)。如果集合内的所有元素都是整数且数量不多,Redis会优先使用intset;否则,将采用hash table实现。
有序集合(Sorted Set/ZSet)
- 应用场景:适用于需要按照某个评分排序的数据,如排行榜、带权重的消息队列等。
- 底层实现:通过跳表(skip list)和哈希表共同实现。跳表支持快速查找和范围查询,而哈希表则用来确保元素的唯一性。
大Key:
问题原因:
- Redis进行大key数据操作耗时会增加
- 占用内存高
- redis单线程处理客户端请求,大key导致主线程占用过久,阻塞其他请求
- 大key增加RDB快照创建与AOF重写的时间,也可能导致主从复制变慢
- 网络IO需要更多时间
解决方案:
- 识别:redis提供
redis-cli --bigkeys
命令扫描数据库以发现大key,或利用第三方工具如redis-rdb-tools
分析RDB文件来识别大key。 - 设计:设计尽量避免大key,可以将key拆分,对于集合类型也可以考虑分片。
- 现行:对于已有大key,可以逐步迁移到新结构;使用批量操作而不是一次性扫描全部数据;设置合理的TTL,定期检查并清除
热Key:
一般指某个指定节点的Key,由于流量高导致节点负载过高,能提前预期规避就规避,否则需要进行热点探测进行相应处理
- 使用本地缓存存储热点
- 将热Key拆分为多个子Key
- 对热Key进行限流
- 热点探测
事务:
常用命令:
- multi:开启一个事务,multi 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中。
- exec:执行队列中所有的命令
- discard:中断当前事务,然后清空事务队列并放弃执行事务
- watch key1 key2 ... :监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
Redis支持事务,但是和传统关系型数据库的事务不同,区别:
- 原子性:Redis中事务保证的是一个队列中的命令作为一个整体,要么全部执行,要么全部不执行,即使有命令执行失败,不会影响执行成功的命令(可以使用lua脚本或单个命令保证原子性)。
- 无隔离级别:Redis中事务的命令在提交前不会被实际执行,所以不存在事务隔离级别问题(客户端发送
muti
命令后,命令会放入队列中,只有exec
命令发送后才开始执行)。 - 乐观锁:Redis使用
watch
命令实现乐观锁,如果检测某些键在exec
前被修改,则事务不会被执行并报错给客户端。
Redis变慢:
- 缓存批量过期,Redis进行主动删除时,其他任务必须等待(不会在慢日志记录)【随机过期时间+可以使用lazy-free后台删除】
内存上限:redis内存淘汰策略如下,淘汰也会阻塞其他操作
- 【有过期时间/全量key】LRU淘汰Key
- 【有过期时间/全量key】随机淘汰Key
- 【有过期时间/全量key】LFU淘汰Key
- 【有过期时间/全量key】淘汰即将过期的Key
- 不淘汰,新写入返回错误
- 持久化导致:Redis进行RDB或AOF rewrite后,主进程会fork子进程去拷贝自己的页表【由于是操作页表,所以获取数据会快,可以使用INFO 查看latest_fork_usec】;刷盘时,如果磁盘IO过高,也会阻塞主线程的write调用
- 内存大页:常规页大小4KB,开启大页后会增加,但是耗时也会增加;Redis使用写时复制,fork期间即使少量数据修改也会申请一个大页
- Swap分区:OS的Swap分区,会用磁盘缓存部分内存
- redis内存碎片整理:也在主线程执行
- 网络IO:网络带宽等导致
使用scan获取所有key:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.List;
public class RedisKeyScanner {
public static void main(String[] args) {
// 建立 Redis 连接
Jedis jedis = new Jedis("localhost", 6379);
// 初始化 SCAN 游标
String cursor = ScanParams.SCAN_POINTER_START;
// 扫描参数
ScanParams scanParams = new ScanParams();
scanParams.match("*"); // 匹配所有模式,可以指定模式
scanParams.count(10); // 每次扫描返回的个数,这个值可调
do {
// 使用 SCAN 命令遍历键
ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
List<String> keys = scanResult.getResult();
cursor = scanResult.getCursor();
// 处理每次返回的键
for (String key : keys) {
System.out.println("Found key: " + key);
}
} while (!cursor.equals(ScanParams.SCAN_POINTER_START)); // SCAN 命令从头到尾遍历
// 关闭 Redis 连接
jedis.close();
}
}