Redis设计与实现

转载收藏一下这篇文章

Redis缓存设计 
设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效.  
1) 缓存击穿:对于一些设置了过期时间的key, 刚好过期的时候,这时候有个高并发的请求,会导致直接访问数据库,危险.(批量放入时这么用的)先把缓存更新,再更新数据库。单个的时候先更新数据库,再更新缓存 
2) 缓存穿透:查询一个一定不存在的数据,导致直接访问数据库。 解决方法:如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 
3) 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效。 随机设置过期时间。

Redis五种基本数据结构 
String类型 
这种数据结构的应用举例,设置过期时间,发短信的时候,设置验证码的过期时间。key一定是String类型,此时value用的是String类型。 
List类型 
应用举例:最近访客可以用这种数据结构。采用双端队列的形式。此类型允许重复 
Hash类型 
举例:在一些不确定有多少字段的时候可以使用。 
Set集合 
应用举例:点赞功能实现,就是有喜欢的集合与不喜欢的集合。不允许重复 
Sorted set集合 
应用举例:排名功能命令举例,分别为String类型,list类型, hash类型,hash过期时间设置, set类型, sorted set类型: 
这里写图片描述

redis设计与实现之SDS 
  https://www.jianshu.com/p/b51bde79be25 
  设置过期时间,发短信的时候验证码的过期时间。还可以实现点赞啥的,高并发。  

 private void sendLoginMsgCode(AclUser aclUser, String sid) {
        long start = System.currentTimeMillis();
        String code = String.valueOf(Math.round(Math.random() * (899999) + 100000));
        String smsMsg = String.format(Constants.MOBILE_CODE_FORMAT, code);
        LOG.info(smsMsg);
      sendMessageService.sendMessage(aclUser.getMobile(), smsMsg);
        final int smsType = 16021;
        smsMsg=code;
        smsPageonService.sendMessage(aclUser.getMobile(), smsMsg,smsType);
        ssoCacheService.setObject(smsKey(sid), 300, code);
        userService.saveMessageCode(aclUser.getLoginName(), smsMsg);
        LOG.info("sendLoginMsgCode {}", System.currentTimeMillis() - start);
}




redis设计与实现之双端队列 
  https://www.jianshu.com/p/e57e126c81c7 
  代码实践:redis list将缓存进行排序,过期的则清掉

private void saveLoginLogCache(String username){
        LoginLogCache loginLogCache = new LoginLogCache();
          loginLogCache.setIp((String)TraceContext.get(Constants.CONTEXT_IP_KEY));
        loginLogCache.setTime(System.currentTimeMillis());
        short type = (short)LoginLogType.USERNAME.getIndex();
        if(TraceContext.get(Constants.CONTEXT_LOGIN_TYPE_KEY) != null){
            type = ((Integer)TraceContext.get(Constants.CONTEXT_LOGIN_TYPE_KEY)).shortValue();
        }
        loginLogCache.setType(type);
        loginLogCache.setUa((String)TraceContext.get(Constants.CONTEXT_UA_KEY));

        long start = System.currentTimeMillis();
        ssoCacheService.lpushObject(buildLoginLogKey(username), loginLogCache);
        ssoCacheService.expire(buildLoginLogKey(username), LOGIN_LOG_EXPIRE);
        ssoCacheService.ltrim(buildLoginLogKey(username), 0, LOGIN_LOG_MAX_SIZE - 1);

        LOG.info("UserLogService.saveLoginLogCache " + (System.currentTimeMillis()-start));
}


redis设计实现之字典 
  这里写图片描述
  dict中 
  1) rehashidx=-1表示不需要再rehash 
   2) iterators记录当前dict中的迭代器数,主要是为了避免在有迭代器时rehash,在有迭代器时rehash可能会造成值的丢失或重复,这就跟在for循环删除增加元素一个道理 
   3)dictType *type;   一些函数 
   4) void *privdata;  一些私有数据 这两个是为了创建多态类型的键值对做准备的. 
   dictht中 
   1) size表示大小 
  2)used表示使用数,sizemask表示hash表大小掩码总是等于size-1用于计算数组索引值.具体的在hash算法中给出 
  3) table为dictEntry **类型 
  dictEntry中 
  存了key,value还有指向dictEntry 类型的指针.链表法解决hash冲突. 
  哈希算法: 
  计算索引位置 
  hash=dict->type->hashFunction(key); 这个函数返回值类型是 unsigned int 就是无符号型的int 
index=hash&dict->ht[x].sizemask . 
  加入新加一个k0,v0 计算出hash=8, 那index= 8&3 =0; 
  当Index值相同时就会产生hash冲突,这时候就采用链地址法解决. 
  rehash: 
  rehash之前: 
  这里写图片描述
  加入现在要拓展到8,那将会变成: 这中间其实是先拓展ht[1] 再将ht[0]复制到ht[1],清空ht[0],再将ht[1]设置成ht[0]方便下次拓展. 
  这里写图片描述
  服务器拓展的时机是跟数据持久化操作有关.下回分解. 
  渐进式rehash 
  由于ht[0]比较大的时候整体rehash会造成服务器出问题所以,会渐进的rehash,比如渐进4个rehashidx会从-1 变到0,1,2,3再到-1 
  这时候就可以进行增删操作,查找时是先找ht[0]再找另外个,增加是增加ht[1]里面的. 
  hash当那些键比较小的时候可以用压缩列表来做. 
sorted set 跳跃表作为底层实现 
从链表开始谈起 
这里写图片描述
 在只有底下一层链表的情况下,如果我们要查找23这个数字时间复杂度是O(n),在加了第二层的情况,查找的情况可以等同于一个二分查找,但是这种结构在插入一个节点的时候会打乱这种严格的2:1关系. 
 跳表 
 这里写图片描述
 跳表在插入数据时层数是随机生成,而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。实际上,这是skiplist的一个很重要的特性,这让它在插入性能上明显优于平衡树的方案。这在后面我们还会提到。 
  结构分析 
  跳表结构总览 
 这里写图片描述
 节点结构  
 其中跨度是用来计算排名的,排名根据score和对象大小排出来: 
 这里写图片描述
 管理节点的结构  
 其中结算最高节点的高度是不包括头节点,当计算某个分数的逆排名就可以用总长度减去正排名: 
 这里写图片描述
 zadd命令举例分析 
 插入的源码:  

for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
            (x->level[i].forward->key < key ||
                (x->level[i].forward->key == key &&
                x->level[i].forward->value < value))) {
            x = x->level[i].forward;
        }
        update[i] = x;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  跳表的查找,插入  
  在查找到该插入的地方之后,只需要改动前后节点的指针就好了,这比红黑树效率高 
 这里写图片描述
  删除过程可以想象,跳表是如何保证在插入的过程中保持比红黑树高效? 
  首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。 
  如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。 
  节点最大的层数不允许超过一个最大值,记为MaxLevel(超过配置文件里面的这个值时,会变成跳表+字典)。 
 * 这种概率能够保证性能的粗略概括就是(从爬k层所要查找的平均步数分析) 
首先分析出一个节点平均层数,泰勒展开可证: 
 
现在很容易计算出: 
当p=1/2时,每个节点所包含的平均指针数目为2; 
当p=1/4时,每个节点所包含的平均指针数目为1.33。这也是Redis里的skiplist实现在空间上的开销 
  其次现在假设我们从一个层数为i的节点x出发,需要向左向上攀爬k层(从已经找到的位置开始逆向考虑,参考光的可逆性)。这时我们有两种可能: 
    如果节点x有第(i+1)层指针,那么我们需要向上走。这种情况概率为p。 
    如果节点x没有第(i+1)层指针,那么我们需要向左走。这种情况概率为(1-p)。 
  用C(k)表示向上攀爬k个层级所需要走过的平均查找路径长度(概率期望),那么: 
    C(k)=(1-p)(C(k)+1) + p(C(k-1)+1) //移项 
    C(k)=1/p+C(k-1) 
    C(k)=k/p 
  这就是说每爬一层,平均查找长度为1/p. 
   总的来说,第1层链表固定有n个节点;第2层链表平均有n*p个节点;第3层链表平均有n*p2个节点;第k层有n*pk个节点,即为n/(1/p)k,可以对比二分查找,第一次n,第二次n/2,第k次n/2k,二分查找的时间复杂度为log n(这种类比是简要类比,实际更复杂可参考William Pugh的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》),则需要查找平均个数为log(1/p) (n),而爬一层需要查找长度为1/p所以总的平均查找为C(log(1/p) (n)*1/p)=log(1/p) (n)*1/p;可以简单理解为log n 利用log (a) (b)=log x b/log x a; 分母理解为常数 
与红黑数对比 
skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。 
在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。 
Redis 数据库 
  这里写图片描述
  Redis 使用的策略: 
  Redis 使用的过期键删除策略是惰性删除加上定期删除, 这两个策略相互配合,可以很好地在合理利用 CPU 时间和节约内存空间之间取得平衡。 
总结: 
  1)数据库主要由 dict 和 expires 两个字典构成,其中 dict 保存键值对,而 expires 则保存键的过期时间。 
  2)据库的键总是一个字符串对象,而值可以是任意一种 Redis 数据类型,包括字符串、哈希、集合、列表和有序集。 
  3)expires 的某个键和 dict 的某个键共同指向同一个字符串对象,而 expires 键的值则是该键以毫秒计算的 UNIX 过期时间戳。 
  4)Redis 使用惰性删除和定期删除两种策略来删除过期的键。 
  5)更新后的 RDB 文件和重写后的 AOF 文件都不会保留已经过期的键。 
  6)当一个过期键被删除之后,程序会追加一条新的 DEL 命令到现有 AOF 文件末尾。 
  7)当主节点删除一个过期键之后,它会显式地发送一条 DEL 命令到所有附属节点。附属节点即使发现过期键,也不会自作主张地删除它,而是等待主节点发来 DEL 命令,这样可以保证主节点和附属节点的数据总是一致的。 
  8)数据库的 dict 字典和 expires 字典的扩展策略和普通字典一样。它们的收缩策略是:当节点的填充百分比不足 10% 时,将可用节点数量减少至大于等于当前已用节点数量。 
RDB持久化 
RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。 RDB保存方式有save,bgsave命令,检查save选项所设置(save 900 1 每隔900秒…)的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令(不会阻塞) 
SAVE命令会阻塞Redis服务器进程,BGSAVE命令会派生出一个子进程。 
RDB与AOF的执行时机,RDB的回复时机。 
这里写图片描述 
在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会和平时有所不同: 
服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbsave调用,防止产生竞争条 
BGREWRITEAOF和BGSAVE两个命令的实际工作都由子进程执行,不能同时执行它们只是一个性能方面的考虑–并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作。 
RDB文件结构: 
REDIS db_version databases EOF check_sum. 
AOF持久化 
AOF 文件的生成过程具体包括命令追加,文件写入,文件同步三个步骤。 
Redis 打开 AOF 持久化功能后,Redis 在执行完一个写命令后,都会将执行的写命令追回到 Redis 内部的缓冲区的末尾。 
接下来,缓冲区的写命令会被写入到 AOF 文件,这一过程是文件写入过程。对于操作系统来说,调用write函数并不会立刻将数据写入到硬盘,为了将数据真正写入硬盘,还需要调用fsync函数,调用fsync函数即是文件同步的过程。只有经过文件同步过程,AOF 文件才在硬盘中真正保存了 Redis 的写命令。appendfsync 配置选项正是用来配置将写命令同步到文件的频率的。 
AOF重写:由于 Redis 会不断地将被执行的命令记录到 AOF 文件里面,所以随着 Redis 不断运行,AOF 文件的体积会越来越大。另外,如果 AOF 文件的体积很大,那么还原操作所需要的时间也会非常地长。 
AOF优缺点: 
AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。 
AOF 文件使用 Redis 命令追加的形式来构造,因此,即使 Redis 只能向 AOF 文件写入命令的片断,使用 redis-check-aof 工具也很容易修正 AOF 文件。 
AOF 文件的格式可读性较强,对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大。 
虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。但在 Redis 的负载较高时,RDB 比 AOF 具好更好的性能保证。 
RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 方式更健壮。 
Redis事件 
文件事件(socket通信的时候),时间事件(执行定期函数的时候) 
主从复制 
Sentinel高可用性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值