redis 模型
redis总体模型
redis的对象都继承自redisObject,其中type包含常用的5种数据结构,encoding是存储数据的编码方式,当对象的refcount为0时(refcount=1时再执行decrRefCount方法)会释放对象占用内存。
typedef struct redisObject {
unsigned type:4; //对象的数据类型,5种
unsigned encoding:4; //对象的编码类型
unsigned lru:LRU_BITS; //least recently used
int refcount; //引用计数
void *ptr; //指向底层数据实现的指针
} robj;
- string
使用char数组存储字符串,会有'\0'作为结束标识符,同时又通过len表示字符串长度和alloc表示使用长度,所以如果buf[]中间出现'\0'也不一定会判定字符串结束了。这样可以兼容C的字符串(以’\0’结尾的字符数组)。在新增字符时,会先判断空间是否足够,不足时先扩容。
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
- list
使用双向链表存储每个节点的数据,并保存表头和结尾。
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;
- hash
使用单向链表存储每个节点数据,数组存放hash表数据
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2]; //两个数组存放数据
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
A 使用两个数组(链表解决hash冲突)完成hash表数据的存储
B 平时使用下标为0的数组存储数据
C reshash时,新建下标为1的数组,然后每次读写数据的同时,将数组0的有限个数据迁移到数据1
D rehash通过rehashidx(初始化为-1,表示不在rehash)指示rehash的进度
E rehash完成后,释放下标0数组的空间,并将下表0指向新数组
F 在rehash期间,新增数据到下标1的数组,读取数据时先从下表0的数组找,找不到再从下标1的数组中找
- set
有两种实现,如果元素是整数且个数较少时,使用intset;其他情况使用hash。
intset初始值默认为INTSET_ENC_INT16,新增元素时会判断元素适用的编码格式。如果范围太小,则可能会将所有元素的编码格式升级成INTSET_ENC_INT32或INTSET_ENC_INT64。当添加元素时需要升级编码,会直接在数组开头或结尾append数据。新增元素时,先查找pos位置(如果已经存在,则返回失败);然后尝试resize数组大小(调用intsetResize,但不一定执行扩容),再写入数据。
typedef struct intset {
uint32_t encoding;
uint32_t length; //集合元素数量
int8_t contents[];//保存元素的数组,元素从小到大排列
} intset;
- zset
zskiplistNode中的zskiplistLevel数组类似于索引指向其他排序的节点。一般zskiplist中的header指向数据为空的一个node,该node的level数组个数(一般为2的n次方)表示跳表的层数。level中zskiplistNode形成的单向链表,每往后一个,表示排序加一。新增元素时,先横向找到该元素所属的排序号,然后随机生成一个数N表示该元素在level中占用的层数(也即该node的level大小),再将前一排序或header中level下标0到N-1指向该node。当集合中两个值的Score相同,这时在跳表中存储会比较这两个值,对这两个值按字典排序存储在跳表结构中。dict存储value与score组成的键值对,而zskiplist则是以score进行排序的链表。
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
ziplist
当保存的对象是小整数值,或者是长度较短的字符串,那么redis就会使用ziplist(压缩列表)来存储数据。ziplist是一系列特殊编码的连续内存块组成的顺序序列数据结构,每个节点除存储数据外,还存储前一个节点和自身长度,便于向前或向后遍历。因为是存储长度已经固定,所以每次插入或删除一个元素时,都需要进行频繁的调用realloc()函数进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失。
typedef struct zlentry {
unsigned int prevrawlensize; /* 编码前驱节点的长度prevrawlen所需要的字节大小*/
unsigned int prevrawlen; /* 前驱节点的长度 */
unsigned int lensize; /* 编码当前节点长度len所需的字节数 */
unsigned int len; /* 当前节点值长度 */
unsigned int headersize; /* 当前节点header的大小 = lensize + prevrawlensize */
unsigned char encoding; /* 编码 */
unsigned char *p; /* 数据 */
} zlentry;
quicklist,zipmap等扩展
redis持久化
RDB和AOF
key/value不宜太大
过期机制
A 主动过期,数据被访问时清理
B 被动过期,随机清理带有过期key中的20个,如果清理比率超过1/4则,循环再清理。
reids达到maxmemory后的处理策略
redis事物
redis单线程的理解
- redis锁
不同机器上的不同客户端操作共享内存数据时发生竞态条件
1:使用WATCH,MULTI, EXEC三个命令和程序中的while循环(注意设置超时);不成功会重试,高并发时影响性能
2:使用setnx,并加上超时参数避免死锁;SET key value [EX seconds] [PX milliseconds] [NX|XX]
def acquire_lock(conn, lockname, acquire_timeout=10, lock_timeout=10):
identifier = str(uuid.uuid4())
lockname = 'lock:' + lock_name
lock_timeout = int(math.ceil(lock_timeout))
end = time.time() + acquire_timeout
while time.time() < end:
if conn.set(lockname, identifier, lock_timeout, 'NX'):
return identifier
elif not conn.ttl(lockname):
conn.expire(lockname, lock_timeout)
time.sleep(.001)
return False
def release_lock(conn, lockname, identifier, release_timeout=10):
pipe = conn.pipeline(True)
lockname = 'lock:' + lockname
end = time.time() + release_timeout
while time.time() < end:
try:
pipe.watch(lockname)
if pipe.get(lockname) == identifier
pipe.multi()
pipe.delete(lockname)
pipe.execute()
return True
pipe.unwatch()
break
except redis.exceptions.WatchError:
pass
return False
- redis计数信号量
同一资源自能被限定个数客户端访问
1 使用zset存储客户端id,时间作为score,(zadd前先zremranebuscore清除过期的key)通过判断新增的记录zrank<limit判断是否计数成功。缺点:不同客户端的系统时间存在差异,可能产生多于预期的信号量
2 使用zset存储客户端id,自定义一个string计数器作为score;额外一个zset存储客户端id,时间作为score。第一个zset通过zrank判断是否能获取信号量,后一个zset用于清除过期的信号量,然后通过zunionstore更新第一个zset。需要注意的是释放信号量时两个zset的清除需要同时进行。
def acquire_semaphore(conn, semname, limit, timeout=10):
identifier = str(uuid.uuid4())
czset = semname + ':owner'
ctr = semname + ':counter'
now = time.time()
pipeline = conn.pipeline(True)
pipeline.zremrangebyscore(semname, '-inf', now-timeout)
pipeline.intersect(czset, {czset: 1, semname: 0})
pipeline.incr(ctr)
counter = pipeline.execute()[-1]
pipeline.zadd(semname, identifier, now)
pipeline.zadd(czset, identifier, counter)
pipeline.zrank(czset, identifier)
if pipeline.execute()[-1] < limit:
return identifier
pipeline.zrem(semname, identifier)
pipeline.zrem(czset, identifier)
pipeline.execute()
return None
def release_semaphore(conn, semname, identifier):
pipeline = conn.pipeline(True)
pipeline.zrem(semname, identifier)
pipeline.zrem(semname + ':owner', identifier)
return pipeline.execute()[0]
信号量会超时,在一些情况下需要进行刷新
def refresh_semaphore(conn, semname, identifier):
if conn.zadd(semname, identifier, time.time()):
release_semaphore(conn, semname, identifier)
return False
return True
在获取信号量的时候,还会存在竟态条件:再竞争最后一个信号量时,即使A进程先获取计数,但只要B进程先执行zadd/zrank,A和B都可成功获取信号量
消除竟态条件可以使用锁
def acquire_semaphore_with_lock(conn, semname, limit, timeout=10):
identifier = acquire_lock(conn, semname, acquire_timeout=.01)
if identifier:
try:
return acquire_semaphore(conn, semname, limit, timeout)
finally:
release_lock(conn, semname, identifier)
redis用例
- 搜索信息自动补全
场景1:查找用户最近联系人
使用list(占用内存少)存储最多100个最近联系人,匹配时通过程序处理。
与redis交互时需要考虑以下三个操作:
A 添加/更新联系人:如果列表中已经存在就删除,然后将其添加到列表最前,最后保证队列长度不超过100(lrem, lpush, ltrim)
B 从联系人列表中删除记录:直接删除(lrem)
C 获取列表:获取数据后,代码中匹配(lrange, startswith)
场景2:给工会成员发送邮件,补全10个
使用zset存储工会成员,根据字符串比较大小规则,向有序集合添加元素来创建查找范围,并在使用zrange取得范围内的元素之后移除之前的元素。(后续优化方案)
需要注意的是:
A 防止多个用户添加/移除相同的元素,需要在该元素上针对每个用户添加唯一标识(uuid)
B 使用WATCH,MULTI, EXEC确保有序集合不会在进行范围查找/取值期间发生变化
C 计算range范围时需要考虑被添加元素;剔除添加的元素后,需校验取值终点不能为-1
D 取得的结果需要过滤其他用户插入的元素(zadd发生在WATCH之前)
场景3:不止匹配关键词开头的信息,还可以跳过字母匹配(类似idea中输入类名查找文件)
A 定义取数结果个数
B 先按关键词全额匹配,如果匹配结果个数已达到,则返回
C 匹配结果个数不足,拆分最后一个字符后,再匹配。在结果筛选出包含最后一个字符的
D 如果筛选的结果不足,拆分最后两个字符,如此循环,直到结果个数达到
- 任务队列
redis list的lpush,blpop,rpush,brpop组合可以实现先进先出队列,后进先出队列(pop命令使用的是阻塞版)
A 如果任务队列有优先级,则可以使用多个队列操作(blpop(queues, 30)会顺序检查队列,弹出队列中第一个非空元素)
B 如果任务需要延迟执行,则通过zset(执行时间戳作为score)存储延迟任务,循环检测此zset,将到期的任务添加进任务队列
- 消息发送接收
A 单个接收者,同任务队列一样,任务替换成消息即可
B 多个接收者,以群组为单位发送消息,用户读取消息。
zset存储群组消息,分值是消息id;zset存储群用户,分值为用户已读消息的最大id;每个用户分配一个zset,存储群组id,分值为群组中已读消息的最大值;另外有自增string,作为群组,用户新建的主键。需要注意创建群时,需要初始化的数据;清理群组已读消息;发送群组消息LOCK。
- 搜索
简单搜索--根据关键词搜索文章,并根据文章的某个属性进行排序
A 每天文章建立反向索引,文章中的每个关键字,有一个对应的set,存储了包含该关键字的文章id
B 根据每个关键字找到对应的set,使用sintersect求交集
C 使用sort命令,根据文章信息(每篇文章使用hash存储其作者,创建时间等)对结果集进行排序
在简单搜索的基础上,如果要实现多条件(属性)组合的最终值排序,则(区别于数据库order by A, B)
A 使用zset存储文章属性的分值,如果有多个属性则用多个zset
B 使用zintersect命令,并设置每个zset的权重,求交集
C 对于非数值类型的属性,可以设定对应的转换算法,将其转换为数值
- 广告定向
根据每个用户所处的位置,和用户浏览的信息(words)推荐广告
A 每个ip有一个set,存储要被展示的广告id
B 使用hash存储广告的默认的价值属性;每个word有一个zset,存储包含该word关键字的广告,word对于广告的贡献者作为分值(初始化为0,后续根据广告点击等事件,每隔一段时间重新计算)
C 根据ip信息,使用union命令得到要被展示的广告
D 根据广告结果集使用zintersect过滤广告价值属性hash中的广告
E 根据广告结果集过滤word(从浏览页面传递过来)对应的集合中的广告,并使用算术平均数求这些word对于每个广告的贡献值
F 根据广告默认价值,和word对应的附加值,对广告结果集进行排序(使用带有权重参数的zunion)
需要记录一段时间内广告的浏览数,点击数,执行数,用于计算广告实际展示效果;记录每个word下广告的浏览数,点击数,执行数,用于计算word对广告的附加值
redis注意事项
keys smember scan
redis set数据缓慢
1是否偶尔出现;2后台bgsave;3是否操作过期属性的key并且value很大
短时间处理大量过期key应对方案
redis exists key应用,如果不是使用pipeline,并且后续会对value值进行操作的话,可以直接考虑get命令,然后判断获取的变量是否为空;通用适用与list中如果存在某个值,则删除等操作的场景
redis集群哨兵