redis in action学习手记

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集群哨兵

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值