0、Redis中Key命令
// 1. 检查给定 key 是否存在
redis 127.0.0.1:6379> EXISTS runoob-new-key
(integer) 0
redis 127.0.0.1:6379> set runoob-new-key newkey
OK
redis 127.0.0.1:6379> EXISTS runoob-new-key
(integer) 1
// 2. 设置过期时间,以秒计算
// 基本语法
redis 127.0.0.1:6379> Expire KEY_NAME TIME_IN_SECONDS
// 实例 (一分钟后删除)
redis 127.0.0.1:6379> SET runooobkey redis
OK
redis 127.0.0.1:6379> EXPIRE runooobkey 60
(integer) 1
// 3. 设置过期时间,以毫秒计算
// 基本语法
PEXPIRE key milliseconds
// 实例
redis> SET mykey "Hello"
"OK"
redis> PEXPIRE mykey 1500
(integer) 1
// 4. 移除key的过期时间,使key永不过期
// 基本语法
redis 127.0.0.1:6379> PERSIST KEY_NAME
// 实例
redis> SET mykey "Hello"
OK
redis> EXPIRE mykey 10 # 为 key 设置生存时间
(integer) 1
redis> TTL mykey
(integer) 10
redis> PERSIST mykey # 移除 key 的生存时间
(integer) 1
redis> TTL mykey
(integer) -1
// 5. 返回key的剩余过期时间 PTTL以毫秒为单位、TTL以秒为单位
// 基本语法
redis 127.0.0.1:6379> PTTL KEY_NAME
// 实例
# key 不存在,返回-2
redis> PTTL key
(integer) -2
# key 存在,但没有设置剩余生存时间,返回-1
redis> SET key value
OK
redis> PTTL key
(integer) -1
# 有剩余生存时间的 key,返回剩余生存时间
redis> PEXPIRE key 10086
(integer) 1
redis> PTTL key
(integer) 6179
1、数据类型
数据类型 | 可以存储的值 | 操作 |
---|---|---|
STRING | 字符串、整数或浮点数 | 对整个字符串或字符串其中一部分执行操作,对整数和浮点数执行自增自减 |
LIST | 列表 | 从两端压入弹出元素;对单个或多个元素进行修剪;只保留一个范围内的元素 |
SET | 无序集合 | 添加获取移出单个元素;检查元素是否存在;计算交并差集;从集合中随机获取元素 |
HASH | 键值对 | 检查键是否存在 |
ZSET | 有序集合 | 根据分值范围或成员获取元素;计算键的排名 |
STRING
String存储时使用的数据结构是简单的key-value类型
get key
通过key获取value
set key value
存入键值对
del key
删除key对应的键值对
LIST
Redis对于List的实现是双向链表,可以支持双向查找和遍历
- 常用场景:分页查询
// rpush: 将value压入list末尾,不会去重
redis 127.0.0.1:6379 > rpush list-key item
(integer) 1
redis 127.0.0.1:6379 > rpush list-key item2
(integer) 2
redis 127.0.0.1:6379 > rpush list-key item
(integer) 3
// lrange:获取list中给定范围的数据
redis 127.0.0.1:6379 > lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
// lindex:获取给定索引下的数据 0为首位 -1 末位
redis 127.0.0.1:6379 > lindex list-key 1
"item2"
// lpop:弹栈
redis 127.0.0.1:6379 > lpop list-key
"item"
redis 127.0.0.1:6379 > lrange list-key 0 -1
1) "item2"
2) "item"
SET
支持去重的列表
// sadd:向集合中添加元素,会去重,添加成功返回1,否则返回0证明该元素已存在
redis 127.0.0.1:6379 > sadd set-key item
(integer) 1
redis 127.0.0.1:6379 > sadd set-key item2
(integer) 1
redis 127.0.0.1:6379 > sadd set-key item3
(integer) 1
redis 127.0.0.1:6379 > sadd set-key item
(integer) 0
// smembers:返回整个集合中的元素
redis 127.0.0.1:6379 > smembers set-key
1) "item"
2) "item2"
3) "item3"
// sismember:判断元素是否在集合中 0表示不在,1表示在
redis 127.0.0.1:6379 > sismember set-key item4
(integer) 0
redis 127.0.0.1:6379 > sismember set-key item
(integer) 1
// srem:移除集合中元素 存在该元素返回1,不存在返回0
redis 127.0.0.1:6379 > srem set-key item2
(integer) 1
redis 127.0.0.1:6379 > srem set-key item2
(integer) 0
redis 127.0.0.1:6379> smembers set-key
1) "item"
2) "item3"
HASH
string类型的field和value的映射表,适合用于存储对象。
// hset: 将key value成堆存储在hash表中,添加成功返回1,重复添加/更新value返回0
redis 127.0.0.1:6379 > hset hash-key sub-key1 value1
(integer) 1
redis 127.0.0.1:6379 > hset hash-key sub-key2 value2
(integer) 1
redis 127.0.0.1:6379 > hset hash-key sub-key1 value1
(integer) 0
// hgetall: 获取hash表中的全部键值对
redis 127.0.0.1:6379 > hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"
// hdel: 删除给定key的键值对,删除成功返回1,不存在返回0
redis 127.0.0.1:6379> hdel hash-key sub-key2
(integer) 1
redis 127.0.0.1:6379> hdel hash-key sub-key2
(integer) 0
// hget: 根据key获取value
redis 127.0.0.1:6379> hget hash-key sub-key1
"value1"
ZSET-有序集合
相比set增加了权重参数score,将元素按照score进行有序排列。
- 常用场景:直播中礼物排行榜…
// zadd: 将元素和对应的score添加进ZSET
redis 127.0.0.1:6379 > zadd zset-key 728 member1
(integer) 1
redis 127.0.0.1:6379 > zadd zset-key 982 member0
(integer) 1
redis 127.0.0.1:6379 > zadd zset-key 982 member0
(integer) 0
// zrange: 获取按scores排序后的数据 withscores是否携带scores
redis 127.0.0.1:6379 > zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
// zrangebyscore: 获取某一scores范围的数据
redis 127.0.0.1:6379 > zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
// zrem: 移除元素
redis 127.0.0.1:6379 > zrem zset-key member1
(integer) 1
redis 127.0.0.1:6379 > zrem zset-key member1
(integer) 0
redis 127.0.0.1:6379 > zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
2、数据结构
字典
dict字典,dictht是字典中的散列表结构,使用拉链法解决哈希冲突。
字典 dict
的实现:
/*
* 字典
*
* 每个字典使用两个哈希表,用于实现渐进式 rehash
*/
typedef struct dict {
// 特定于类型的处理函数
dictType *type;
// 类型处理函数的私有数据
void *privdata;
// 哈希表(2 个)
dictht ht[2];
// 记录 rehash 进度的标志,值为 -1 表示 rehash 未进行
int rehashidx;
// 当前正在运作的安全迭代器数量
int iterators;
} dict;
哈希表 dictht
的实现:
/*
* 哈希表
*/
typedef struct dictht {
// 哈希表节点指针数组(俗称桶,bucket)
dictEntry **table;
// 指针数组的大小
unsigned long size;
// 指针数组的长度掩码,用于计算索引值
unsigned long sizemask;
// 哈希表现有的节点数量
unsigned long used;
} dictht;
哈希表 dictEntry
中的节点:
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 链往后继节点
struct dictEntry *next;
} dictEntry;
字典的实现图示:
扩容机制
dictAdd 在每次向字典添加新键值对之前, 都会对哈希表 ht[0] 进行检查, 对于 ht[0] 的 size 和 used 属性, 如果它们之间的比率 ratio = used / size 满足以下任何一个条件的话,rehash 过程就会被激活:
- 自然 rehash : ratio >= 1 ,且变量 dict_can_resize 为真。(没有执行 BGSAVE 或 BGREWRITEAOF时,即没有进行持久化时)
- 强制 rehash : ratio 大于变量 dict_force_resize_ratio (目前版本中, dict_force_resize_ratio 的值为 5 )。
Rehash执行过程
- 创建一个比 ht[0] -> table 更大的 ht[1] -> table;
- 将 ht[0] -> table 中的所有键值对迁移到 ht[1] -> table;
- 将原有 ht[0] 的数据清空,并将 ht[1] 替换为新的 ht[0];
为了保证redis的响应速度,rehash过程会被均摊到每个操作中,称为渐进式Rehash
渐进式Rehash
具体步骤:
- 为ht[1]分配空间,同时持有两个哈希表(一个空表,一个有数据)
- 维持一个计数器rehashidx(记录当前rehash进行到 ht[0] 哪个索引位置上),初始值为0
- 每次对字典增删改查,会顺带将ht[0]中的第一个不为空的索引上的数据迁移到ht[1],rehashidx++
- 直到rehash操作完成,rehashidx值设为-1
其他措施:
- rehash过程中字典会同时使用两个哈希表,所有的查找、删除等操作,需要同时在 ht[0] ht[1]上进行
- 添加时,直接添加到ht[1],保证ht[0]的节点数量在整个rehash过程中只减不增。
跳表
有序集合的底层实现之一
基于多指针有序链表实现。
查找时从上层指针开始查找,找到对应区间后再到下一层寻找。
与红黑树相比的优点:
- 插入速度快,不需要旋转等维护平衡性
- 实现简单
- 支持无锁操作
3、过期时间与内存淘汰机制
token,短信验证码等需要进行过期时间设置
在设置存储元素时设置其过期时间,到达过期时间数据会失效,将这部分数据删除有两种方式:
定期删除
redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
惰性删除
定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。
内存淘汰
若存在大量过期数据存储在Redis中,可能会导致内存耗尽,因此Redis会根据内存淘汰机制对内存进行清理。
目前Redis中存在以下几种内存淘汰策略:
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
noeviction | 禁止驱逐数据 |
4、持久化
RDB持久化
将某个时间点的所有数据都存放到硬盘上。
AOF持久化
将写命令添加到AOF文件(Append Only File)末尾,以保证文件能够记录对Redis的所有更改操作。
AOF持久化需要设置同步选项,确保写命令同步到磁盘文件上的时机。
同步选项:
- always:同步每个写命令(对性能影响较大)
- everysec:每秒同步
- no:操作系统决定何时同步(对性能提升不大,但会增加数据丢失风险)
5、分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。
假设有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,… ,有不同的方式来选择一个指定的键存储在哪个实例中。
- 最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
根据执行分片的位置,可以分为三种分片方式:
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
- 服务器分片:Redis Cluster。
参考资料:
JavaGuide的Redis讲解
Redis中数据类型-《Redis In Action》
Redis设计与实现
CS-Notes
Redis中文网