Redis学习笔记

一文搞懂redis

Redis是什么?

NoSQL= not only sql,而不是no sql

Redis = Remote Dictionary Server (远程调用字典服务)

Redis是开源的 内存中的数据结构存储系统,它可以用作 数据库、 缓存和 消息中间件,它支持多种类型的数据结构,如 字符串strings, 散列hashes, 列表lists, 集合sets, 有序集合sorted sets与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。

Redis还内置了 复制(replication),LUA脚本(Lua ing), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)

通用命令

  • keys:查询所有的key,支持正则
  • dbsize:计算key的总数
  • exists key:是否存在key
  • del key [key...]:删除key。(del key1,key2...)
  • expire key seconds:设置过期时间
  • ttl key:查看剩余过期时间
  • persist key:去掉过期时间
  • type key

数据结构

redis的五个类型

  • string:动态字符串
  • hash:哈希
  • list:列表
  • set:集合
  • zset:有序集合

redis底层的八种数据结构

  •  REDIS_ENCODING_INT(long 类型的整数) REDIS_ENCODING_EMBSTR embstr (编码的简单动态字符串) REDIS_ENCODING_RAW (简单动态字符串) REDIS_ENCODING_HT (字典) REDIS_ENCODING_LINKEDLIST (双端链表) REDIS_ENCODING_ZIPLIST (压缩列表) REDIS_ENCODING_INTSET (整数集合) REDIS_ENCODING_SKIPLIST (跳跃表和字典)

redis的整体存储架构是一个大的hashmap,内部是数组实现的hash,key冲突用拉链法,挂链表去解决,每个dicEntry为一个key\value对象,value为自定义的redisObject

head       head       head       head    →   ...
            
dictEntrydictEntrydictEntry
   
dictEntry

一、string数据结构

  • 保存值为整数,大小不超过long的范围,使用整数存储
  • 当字符串长度不超过44字节,用embstr编码,只分配一次内存空间,是连续的内存,只允许读,如果修改数据就变成raw编码
  • 大于44字节,用raw编码

为什么是44字节?,内部存储数据的结构体:redisObject和sdshdr8,一个占16字节,一个占8字节,初始最小分配为64字节,64-16-4=44字节

二、List

1、Redis3.2之前的底层实现方式:压缩列表ziplist 或者 双向循环链表linkedlist。当list存储的数据量比较少且同时满足下面两个条件时,list就使用ziplist存储数据:

  • list中保存的每个元素的长度小于 64 字节;

  • 列表中数据个数少于512个

2、Redis3.2及之后的底层实现方式:quicklist

quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点。

ziplist

ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。如下图所示,redisObject对象结构中ptr所指向的就是一个ziplist。整个ziplist只需要malloc一次,它们在内存中是一块连续的区域。

为什么数据量大时不用ziplist?

因为ziplist是一段连续的内存,插入的时间复杂化度为O(n),而且每当插入新的元素需要realloc做内存扩展;而且如果超出ziplist内存大小,还会做重新分配的内存空间,并将内容复制到新的地址。如果数量大的话,重新分配内存和拷贝内存会消耗大量时间。所以不适合大型字符串,也不适合存储量多的元素

quickList

快速列表是ziplist和linkedlist的混合体,是将linkedlist按段切分,每一段用ziplist来紧凑存储,多个ziplist之间使用双向指针链接。


三、hash

当Hash中数据项比较少的情况下,Hash底层才用压缩列表ziplist进行存储数据,随着数据的增加,底层的ziplist就可能会转成dict

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

typedef struct dictht {
    //指针数组,这个hash的桶
    dictEntry **table;
    //元素个数
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dictEntry大家应该熟悉,在上面有讲,使用来真正存储key->value的地方
typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        // 指向具体redisObject
        void *val;
        // 
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

个人理解,一个dict字典有两个hash表,一个放旧的,一个放新的。每个hash表 有个指针数组,存hash的桶(拉链法)。

dict:

虽然dict结构有两个hashtable,但是通常情况下只有一个hashtable是有值的。但是在dict扩容缩容的时候,需要分配新的hashtable,然后进行渐近式搬迁,这时候两个hashtable分别存储旧的hashtable和新的hashtable。搬迁结束后,旧hashtable删除,新的取而代之。

渐进式rehash

所谓渐进式rehash是指我们的大字典的扩容是比较消耗时间的,需要重新申请新的数组,然后将旧字典所有链表的元素重新挂接到新的数组下面,是一个O(n)的操作。但是因为我们的redis是单线程的,无法承受这样的耗时过程,所以采用了渐进式rehash小步搬迁,虽然慢一点,但是可以搬迁完毕。

扩容条件

dict结构体的hashtable用了链地址法解决hash冲突,hash表的数组中的每一项都是entry链表的头结点,当hash表中的元素个数等于第一维数组长度的时候就会开始扩容,扩容的大小是原数组的两倍。不过redis在做持久化RDB时,为了减少内存页的过多分离,就不会去扩容,因为系统需要更多的开销去回收内存,但是如果hash表里元素个数达到了第一维数组长度的5倍的时候吗,就会强制扩容,不管是在持久化。

缩容条件

hash表数据逐渐删除的时候,redis会对hash表缩容来减少第一维数组长度的空间占用。条件是元素个数低于数组长度的10%,且缩容时不考虑持久化问题

步骤

  1. 为ht[1]分配空间,让字典有两个哈希表
  2. 定时维持一个索引计数器变量rehashidx,设置其值为0,表示rehash开始
  3. 在rehash时,每次对字典执行CRUD操作时,程序除了操作外还会将ht[0]中的数据rehash到h[1]中,并且将rehashidx加一。
  4. 当h[0]数据全部转移到了h[1],将rehashidx设置成-1,表示rehash结束。

(采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。特别的在进行rehash时只能对h[0]元素减少的操作,如查询和删除;而查询是在两个哈希表中查找的,而插入只能在ht[1]中进行,ht[1]也可以查询和删除。)

        5、将h[0]释放,然后将h[1]设置成h[0],再分配一个新的空白hash表给h[1]


持久化机制

RDB

将某一时刻的数据保存到硬盘的文件中,有save和bgsave

恢复数据快,使用子进程进行持久化

但是服务器宕机,RDB会使得某一时刻的数据丢失,save命令会阻塞主进程,用bgsave的话是用子进程fork,但是如果数据量太大也还是会阻塞,且fork也耗费内存

AOF

记录每一次写操作命令

追加日志文件,对服务器性能影响小,但生成的日志文件大,恢复数据比RDB慢

为什么使用redis缓存

  1. 加速读写,因为缓存通常都是全内存的,而存储层通常读写性能不够强悍(例如MySQL),通过缓存的使用可以有效地加速读写,优化用户体验。
  2. 降低后端负载:帮助后端减少访问量和复杂计算(例如很复杂的SQL语句),在很大程度降低了后端的负载。

缓存更新策略

  1. LRU/LFU/FIFO算法剔除:剔除算法通常用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。例如Redis使用maxmemory-policy这个配置作为内存最大值后对于数据的剔除策略。
  2. 超时剔除:通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如Redis提供的expire命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以为其设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。但是涉及交易方面的业务,后果可想而知
  3. 主动更新:应用方对于数据的一致性要求高,需要在真实数据更新后,立即更新缓存数据。例如可以利用消息系统或者其他方式通知缓存更新。

三大缓存问题

一、缓存穿透

不断被访问redis和mysql不存在的数据,导致崩溃

解决方案

  1. 验证拦截:接口层做校验,如用户权限,或者拦截 id<0的请求
  2. 缓存空数据:就算没有数据也缓存,但时间要设置短一点,1秒等。
  3. 布隆过滤器:一个类似hashmap的数据结构,作用是可以确认某个数一定不存在。

    当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了。如果这些点有任何一个 0,则被检索元素一定不在;如果都是 1,则被检索元素很可能在。

缓存空数据与布隆过滤器的比较

缓存空数据与布隆过滤器都能有效解决缓存穿透问题,但使用场景有着些许不同;

  • 当一些恶意攻击查询查询的key各不相同,而且数量巨多,此时缓存空数据不是一个好的解决方案。因为它需要存储所有的Key,内存空间占用高。并且在这种情况下,很多key可能只用一次,所以存储下来没有意义。所以对于这种情况而言,使用布隆过滤器是个不错的选择;

  • 而对与空数据的Key数量有限、Key重复请求效率较高的场景而言,可以选择缓存空数据的方案。


二、缓存击穿

缓存击穿是指当前热点数据存储到期时,多个线程同时并发访问热点数据。因为缓存刚过期,所有并发请求都会到数据库中查询数据。

解决方案

  • 将热点数据设置为永不过期;

  • 加互斥锁:互斥锁可以控制查询数据库的线程访问,但这种方案会导致系统的吞吐量下降,需要根据实际情况使用。


三、缓存雪崩

缓存雪崩发生有几种情况,比如大量缓存集中在或者缓存同时在大范围中失效,出现了大量请求去访问数据库,从而导致CPU和内存过载,甚至停机。

一个简单的雪崩过程:

  1. Redis 集群产生了大面积故障;

  2. 缓存失败,此时仍有大量请求去访问 Redis 缓存服务器;

  3. 在大量 Redis 请求失败后,这些请求将会去访问数据库;

  4. 由于应用的设计依赖于数据库和 Redis 服务,很快就会造成服务器集群的雪崩,最终导致整个系统的瘫痪

  • 【事前】高可用缓存:高可用缓存是防止出现整个缓存故障。即使个别节点,机器甚至机房都关闭,系统仍然可以提供服务,Redis 哨兵(Sentinel) 和 Redis 集群(Cluster) 都可以做到高可用;

  • 【事中】缓存降级(临时支持):当访问次数急剧增加导致服务出现问题时,我们如何确保服务仍然可用。在国内使用比较多的是 Hystrix,它通过熔断、降级、限流三个手段来降低雪崩发生后的损失。只要确保数据库不死,系统总可以响应请求,每年的春节 12306 我们不都是这么过来的吗?只要还可以响应起码还有抢到票的机会;

  • 【事后】Redis备份和快速预热:Redis数据备份和恢复、快速缓存预热。


Redis为什么快?

  • 全内存操作,内存的读写速度是磁盘的好几倍。
  • 单线程操作。通过I\O多路复用技术,将网络请求封成一个队列,单线程地由redis的事件处理器再逐一取出分发到对应的事件,这样就没有了多线程的竞争。加锁和上下文切换等操作。当然其他的地方该多线程还是多线程
  • 简单的数据结构和数据操作方法,使得时间复杂度大大降低

过期策略:主动过期+惰性过期 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值