Redis 具有如下特点:
Redis 以 hash 表存储键值对。使用拉链法解决 hash 冲突。拉链法的示例代码如下图
N = 100010
class HashTable:
"""拉链法实现哈希表"""
def __init__(self):
self.h = [-1] * N
self.idx = 0
self.e = [0] * N
self.ne = [0] * N
self.MOD = N
def query(self, x):
curr = self.h[x % self.MOD]
while curr != -1:
if self.e[curr] == x:
return True
curr = self.ne[curr]
return False
def add(self, x):
self.e[self.idx] = x
self.ne[self.idx] = self.h[x % self.MOD]
self.h[x % self.MOD] = self.idx
self.idx += 1
hash_table = HashTable()
n = int(input())
for _ in range(n):
arr = input().split()
if arr[0] == 'I':
hash_table.add(int(arr[1]))
if arr[0] == 'Q':
if hash_table.query(int(arr[1])):
print("Yes")
else:
print("No")
渐进式 Hash:
编码:
string
hash
list
set
zset
用于解决基数(不重复元素个数)统计问题
算法是基于概率统计的估计算法,有误差。Redis 误差为 0.81%
空间复杂度为 (O(m * loglogN)),空间复杂度极低。在 Redis 中使用稀疏存储结构和密集存储结构保存,最多占用12KB 内存(16384个桶)。
算法原理基于 伯努利试验 和 分桶取平均,缩小误差
伯努利试验,讲的是我们 N 次抛硬币(假设出现正面反面的概率相同),如果直到 k 次才出现正面,那么 N 大约为 2 的 k 次方。
分桶取平均,指的是将 N 个数据均为为 m 个桶,每个桶单独 k 的值(1 出现的最大位数),取平均(有多种取法,常见的是调和平均)
Redis 中使用 HyperLogLog 的命令为:
bitmap
事件
文件事件:Redis 基于 Reactor 模式(TODO: 搞清楚 Reactor 模式),使用 I/O多路复用(multiplexing)监听文件事件。
时间事件:定时任务,周期任务。Redis 使用链表(time_events)保存时间事件。
def processTimeEvents():
# 遍历事件是否已经到达
for time_event in all_time_events():
# 检查事件是否到达
if time_event.when <= unix_ts_now():
# 事件已到达
# 执行事件处理器,并获取返回值
retval = time_event.timeProc()
# 如果这是一个定时事件
if retval == AE_NOMORE:
# 那么将该事件从服务器中删除
delete_from_event_from_server(time_event)
else:
# 更新事件的 when 属性
update_when(time_event, retval)事件处理逻辑
代码在 ae.c/aeProcessEvents 函数。伪代码如下:
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
# 计算最接近时间,还有多少毫秒到达
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已经到达,那么 remaind_ms 取值位负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval 结构
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件发生,最大阻塞由传入的 timeval 结构决定
# 如果 remaind_ms 的值为 0, 那么 aeApiPoll 调用之后马上返回,不阻塞
aeApiPoll(timeval)
# 处理所有已经产生的文件事件
processFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()Redis 单条命令保证操作的原子性;但是 Redis 事务(MULTI 操作)仅仅保证将一组命令(入队的命令)有序执行,并不保证这些操作的原子性。
MULTI EXEC/DISCARD 命令
> MULTI
OK
> zadd salary 100 wangmeng
QUEUED
> zadd salary xxx zhangsan # 执行错误,不影响接下来命令的执行。
QUEUED
> zadd salary 200 wangxiaoyu
QUEUED
> EXEC
1) 0
2) ERR value is not a valid float
3) 0> MULTI
OK
> zadd salary 100 wangmeng
QUEUED
> zadd salary xxx # 入队错误,会导致所有命令都执行不了。
QUEUED
> zadd salary 200 wangxiaoyu
QUEUED
> EXEC
(error) ERR wrong number of arguments for 'zadd' commanddef MULTI():
# 打开事务标识
client.flags |= REDIS_MULTI
# 返回 ok 回复
replyOk()typedef struct redisClient {
...
// 事务状态
multiState mstate;
...
}typedef struct multiState {
// 事务队列, FIFO 顺序
multiCmd *commands;
// 已入队命令计数
int count;
} multiState;typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
} multiCmd;def EXEC():
# 创建空白的回复队列
reply_queue = []
# 遍历事务队列中的每个项
# 读取命令的参数,参数的个数,以及要执行的命令
for argv, argc, cmd in client.mstate.commands:
# 执行命令,并取得命令的返回值
reply = execute_command(cmd, argv, argc)
# 将返回值追加到回复队列队尾
reply_queue.append(reply)
# 移除 REDIS_MULTI 标识,让客户端回到非事务状态
client.flags &= ~REDIS_MULTI
# 清空客户端的事务状态,包括:
# 1) 清零入队命令计数器
# 2) 释放事务队列
client.mstate.count = 0
release_transaction_queue(client.mstate.commands)
# 将事务的执行结果返回给客户端
send_reply_to_client(client, rely_queue)WATCH 命令
可以使用 WATCH 来监视变量,如果变量在期间被操作过,就拒绝执行 MUTLI/EXEC 事务。这是个乐观锁的逻辑。
两个客户端执行命令的过程:
| 时间 | 客户端A | 客户端B |
|---|---|---|
| T1 | WATCH "name" | |
| T2 | MULTI | |
| T3 | SET "name" "peter" | |
| T4 | SET "name" "john" | |
| T5 | EXEC |
watched_keys
typedef struct redisDb {
...
// 正在被 WATCH 监视的键
dict *watched_keys;
...
}所有对数据库进行修改的命令,比如 SET, LPUSH, SADD, ZREM, DEL, FLUSHDB 等等,在执行之后,都会调用 multi.c/touchWatchedKey 函数对 watched_keys 字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么 touchWatchKey 函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS 标识打开,标识该客户端的事务安全性已经被破坏。
def touchWatchKey(db, key):
# 如果 key 存在于数据库的 watched_keys 字典中
# 那么说明至少有一个客户端在监视这个 key
if key in db.watched_keys:
# 遍历所有监视键 key 的客户端
for client in db.watched_keys[key]:
# 打开标识
client.flags |= REDIS_DIRTY_CAS判断事务是否安全,客户端执行 EXEC 命令时,会首先检查 REDIS_DIRTY_CAS 标识,如果该标识被打开,说明事务不安全,客户端拒绝执行事务;否则客户端执行事务。
rdb
aof(Append Only File)
rdb + aof: TODO: 给个配置案例。
主从
哨兵
集群
String
Hash
List:
Set:
ZSet:
HyperLogLog: