一、什么是redis
- 开源的,支持网络,基于内存,键值对存储数据库
- redis默认创建16个数据库,通过select语句可以切换数据库, 即单个实例可以有多个数据库,类似MySQL
二、redis底层实现
2.1. 数据结构
2.1.1 简单的动态字符串
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
- 空间预分配, buf的长度可能不是实际字符串的长度,通过len获取
2.1.2 链表
/*
* 双端链表节点
*/
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
/*
* 双端链表结构
*/
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;
- 双端链表
- 通过list 来维护整个链表
2.1.3 字典-hashtable
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
/*
* 字典
*/
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
- 机遇哈希表实现字典
- 通过链表(小尾巴)解决哈希可能出现的冲突
- 两个哈希表,来实现哈希表的扩缩容
- 通过rehashidx来计数,渐进式地把ht[0]的键值对,迁移到ht[1] 双写,避免集中式带来的cpu压力
2.1.4 跳表
/* ZSETs use a specialized version of Skiplists */
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
/*
* 跳跃表
*/
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
- 一个List维护表头、表尾,和节点数量
- 一个ListNote包含obj, 多个forward指针(跨度), 一个backward指针,一个score
- ListNote是按score的从小到大排序的, 当查找到某个节点时, 所经过的跨度(span)之和,即是该节点的排序
2.2 核心对象-redisObject
typedef struct redisObject {
// 类型 string list hash set zset
unsigned type:4;
// 编码 比如string 的编码可以是int , raw(sds)
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
- redisObject为对底层数据的封装, 是最小的操作单位
- 针对不同的使用场景可以用相同的对象来实现
- 引入了引用计数和lru字段,来对不用的对象进行回收、优先删除(golang 用的三色标(white\grey\black)记法)
三、单机数据库实现
3.1 数据库
- 过期键删除策略:惰性(访问删除) 和 定时删除(相对空闲时)配合
3.2 数据持久化–rdb&aof
3.2.1 全量策略
即rdb快照,通过配置实现n分钟内对数据库修改了m次(所以这里维护一个计数, save后重置),则从内存dump数据形成rdb文件进行保存到相应目录。
- 原理:当redis需要做持久化时,redis会fork一个子进程;子进程将数据写到磁盘上一个临时RDB文件中;当子进程完成写临时文件后,将原来的RDB替换掉,这样的好处就是可以copy-on-write
- 优点:因为存储的快照文件为内存映射,所以恢复快。缺点:断电时会导致部分数据丢失
3.2.2 增量策略
aof日志,将执行的命令写入txt文件进行记录
- 为了进行命令的合并, 需要进行重写。 由子进程进行重写,防止阻塞。
- 为了解决父子进程带来的数据不一致问题, 会有一块缓冲区来记录,重写过程中,写命令的操作。 待子进程重写完, 再告知父进程把缓冲区的内容写到新AOF文件中
- 优点:数据不容易丢失。缺点:恢复速度慢
3.3 事件
3.3.1 文件事件
accept read write connect req rsp sockets套接字->I/O多路复用(就绪的事件)->事件分发器->处理器(accept, 命令请求等) loop会从就绪队列中,取出事件进行处理。
3.3.2 时间事件
/* Time event structure
*
* 时间事件结构
*/
typedef struct aeTimeEvent {
// 时间事件的唯一标识符
long long id; /* time event identifier. */
// 事件的到达时间
long when_sec; /* seconds */
long when_ms; /* milliseconds */
// 事件处理函数
aeTimeProc *timeProc;
// 事件释放函数
aeEventFinalizerProc *finalizerProc;
// 多路复用库的私有数据
void *clientData;
// 指向下个时间事件结构,形成链表
struct aeTimeEvent *next;
} aeTimeEvent;
- 将所有的时间事件,放到一个链表,每次tick 判断是否有相应的事件需要处理
- 正常模式下,只有一个serverCron时间事件
- 当然,一定的误差还是有的
3.4 客户端与服务器
/* With multiplexing we need to take per-client state.
* Clients are taken in a liked list.
*
* 因为 I/O 复用的缘故,需要为每个客户端维持一个状态。
*
* 多个客户端状态被服务器用链表连接起来。
*/
typedef struct redisClient {
// 套接字描述符
int fd;
// 当前正在使用的数据库
redisDb *db;
// 当前正在使用的数据库的 id (号码)
int dictid;
// 客户端的名字
robj *name; /* As set by CLIENT SETNAME */
// 查询缓冲区
sds querybuf;
// 查询缓冲区长度峰值
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size */
// 参数数量
int argc;
// 参数对象数组
robj **argv;
// 记录被客户端执行的命令
struct redisCommand *cmd, *lastcmd;
// 创建客户端的时间
time_t ctime; /* Client creation time */
// 客户端最后一次和服务器互动的时间
time_t lastinteraction; /* time of the last interaction, used for timeout */
// 客户端的输出缓冲区超过软性限制的时间
time_t obuf_soft_limit_reached_time;
// 客户端状态标志
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
// 当 server.requirepass 不为 NULL 时
// 代表认证的状态
// 0 代表未认证, 1 代表已认证
int authenticated; /* when requirepass is non-NULL */
// 用于保存主服务器传来的 RDB 文件的文件描述符
int repldbfd; /* replication DB file descriptor */
// 从服务器的监听端口号
int slave_listening_port; /* As configured with: SLAVECONF listening-port */
// 事务状态
multiState mstate; /* MULTI/EXEC state */
// 阻塞类型
int btype; /* Type of blocking op if REDIS_BLOCKED. */
// 阻塞状态
blockingState bpop; /* blocking state */
// 被监视的键
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
// 这个字典记录了客户端所有订阅的频道
// 键为频道名字,值为 NULL
// 也即是,一个频道的集合
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
// 链表,包含多个 pubsubPattern 结构
// 记录了所有订阅频道的客户端的信息
// 新 pubsubPattern 结构总是被添加到表尾
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
sds peerid; /* Cached peer ID. */
/* Response buffer */
// 回复偏移量
int bufpos;
// 回复缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
四、独立功能实现
4.1 事务(ACID)
- MULTI, 将多个执行操作放入queued队列中,exec后再执行
- WATCH 若某个key被修改了, 对应监听的客户端,会打上REDIS_DIRTY_CAS 标签,标识事务安全性已被破坏,exec会执行失败
- 不支持回滚功能
- 单进程, 事务串行运行
- 持久性 rdb Or aof
五、常用命令
1. 进程命令
- stop: redis-server: kill -9;
- restart: /usr/local/bin/redis-server restart /etc/redis/redis.conf
- 设置密码: config set requirepass qqgame
2. 操作命令
- 可用expire或者pexpire命令,以秒或者毫秒设置某个key的生存时间,到期后服务器会自动删除键
- TTL命令和PTTL命令返回一个键的生存时间
- PERSIST可以已出一个键的生存时间
- 其它基本命令
六、单线程设计下的分布式实现
1. 主从同步
slave 起来后,向master发同步请求, master将快照给slave, slave通过快照载入数据,以后每次请求, master都发一份给slave
2. 实现-tredis
- 单线程下, 乐观锁、事务等都可以玩。 在一个队列里依次执行,不会产生并发问题
- 分布式后, 就要考虑分布式锁的各种问题了(乐观锁), 做了层proxy, 相同的key路由到后端某台redis实例. 所以单key不能过分大, 比如上亿。
3. 为何单线程还能这么快
- 基于内存
- 数据结构简单
- 上下文切换
- 瓶颈可能是内存大小和带宽,不是cpu, 所以就使用单线程了(官方)