缓存原理&设计(Redis)

缓存的使用场景:

1. DB缓存,减轻服务器压力  指优先访问缓存, 没有命中找DB

2. 提高系统响应 解决频繁IO而无法响应

3. 做Session分离, 多个服务器共享Session信息

4. 做分布式锁, 控制多个进程并发下产生的问题,以及控制时序性,使用Redis实现的setNX

5. 做乐观锁,Redis可以实现乐观锁 watch + incr

缓存的读写模式:

1. Cache Aside Pattern(常用)

Cache Aside Pattern(旁路缓存),是最经典的缓存+数据库读写模式。

读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

更新的时候,先更新数据库,然后再删除缓存。
为什么是删除缓存,而不是更新缓存呢?
a. 缓存的值是一个结构: hash list ,更新数据需要遍历
b. 懒加载,使用的时候才更新缓存. 也可以采用异步的方式填充缓存
高并发脏读的三种情况

        1、先更新数据库,再更新缓存 

        2、先删除缓存,再更新数据库

        3、先更新数据库,再删除缓存(推荐)

2. Read/Write Through Pattern

3. Write Behind Caching Pattern

缓存架构的设计思路:

1. 多层次, 分布式缓存宕机,本地缓存还可以使用
2. 数据类型, Value是字符串或整数, Value 的值比较大(大于 100K)只进行setter, gtter(采用Memcached)
3.  复杂数据类型 Value是 hash set list zset 需要存储关系,聚合,计算 可采用Redis
4. 分布式缓存集群方案( Redis) 哨兵 +主从   RedisCluster
5. 缓存的数据结构设计: 缓存的数据是经常访问的;  需要存储关系,聚合,计算等;比如某个用户的帖子、用户的评论。
key UID+ 时间戳 ( 精确到天 ) 评论一般以天为计算单位
value Redis Hash 类型。 fifield id content
expire :设置为一天

Redis数据类型:

String:

应用场景:
1 key 和命令是字符串
2 、普通的赋值
3 incr 用于乐观锁
incr :递增数字,可用于实现乐观锁 watch( 事务 )
4 setnx 用于分布式锁
value 不存在时采用赋值,可用于实现分布式锁
eg: setnx name zhangsan    当再一次复制时失败
      set age 18 NX PX 1000   NX 原子操作  PX 过期时间
      

list列表类型:     

        list列表类型可以存储有序、可重复的元素
        获取头部或尾部附近的记录是极快的
        list的元素个数最多为 2^32-1 个( 40 亿)
应用场景:
1 、作为栈或队列使用
列表有序可以作为栈和队列使用
2 、可用于各种列表,比如用户列表、商品列表、评论列表等。

set集合类型:

命令: 

无序, 唯一性

应用场景: 适用于不能重复的且不需要顺序的数据结构  比如:关注的用户,还可以通过spop进行随机抽奖

sortedset有序集合类型:

元素本身是无序不重复的, 每个元素关联一个分数(score), 可按分数排序,分数可重复
zadd key score1 member1 score2 member2 ...

zrem key mem1 mem2 ....  删除成员

zcount key min max 返回区间的个数

zcard key  获得元素数量

zincrby key increment member   在集合的member上加increment

zscore key member   获得集合中的分数
zrank key member     获得集合中 member 的排名(按分值从 小到大)
zrevrank key member  按分值从大到小排序
zrange key start end    指定区间, 按分数递增排序
zrevrange key start end   指定区间, 按分数递减排序
应用场景:
由于可以按照分值排序,所以适用于各种排行榜。比如:点击排行榜、销量排行榜、关注排行榜等。

hash类型(散列表)

Redis hash 是一个 string 类型的 fifield value 的映射表,它提供了字段和字段值的映射。
hset key fifield value   赋值  不区分新增 | 添加
hmset fifield1 value1 fifield2 value2   批量赋值
hsetnx key fifield value    赋值 存在不操作
hexists key fifiled    查看是否存在
hget key fifield    获取某个字段
hmget key fifield1 fifield2 ...  获取多个字段
hincrby key fifield increment    该字段自增
应用场景:
对象的存储 ,表数据的映射

stream数据流类型

RedisObject结构:

typedef struct redisObject { 
    unsigned type:4;//类型 五种对象类型 
    unsigned encoding:4;//编码 
    void *ptr;//指向底层实现数据结构的指针 //... 
    int refcount;//引用计数 //... 
    unsigned lru:LRU_BITS; //
    LRU_BITS为24bit 记录最后一次被命令程序访问的时间 
    //... 
}robj;

type:  指存储当前值是什么类型

REDIS_STRING( 字符串 ) REDIS_LIST ( 列表 ) REDIS_HASH( 哈希 ) REDIS_SET( 集合 )      REDIS_ZSET( 有 序集合) 。    命令: type  key
4 encoding:  指每个对象都有着不同的编码格式,  命令: object  encoding key
Redis 通过 encoding 属性为对象设置不同的编码
对于少的和小的数据, Redis 采用小的和压缩的存储方式,体现 Redis 的灵活性
大大提高了 Redis 的存储量和执行效率
比如 Set 对象: intset : 元素是 64 位以内的整数 hashtable:元素是 64 位以外的整数
24 LRU :  记录着最后一次访问的时间, 高16为记录时间戳, 低8为记录访问计数
refcount
        refcount 记录的是该对象被引用的次数,类型为整型。
        refcount 的作用,主要在于对象的引用计数和内存回收。
        当对象的refcount>1 时,称为共享对象
        Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原的对象。
ptr:  指着具体的数据   ,比如: set hello world ptr 指向包含字符串 world SDS
SDS的优势:
1 SDS C 字符串的基础上加入了 free len 字段,获取字符串长度: SDS O(1) C 字符串是
O(n)
buf 数组的长度 =free+len+1
2 SDS 由于记录了长度,在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
3 、可以存取二进制数据,以字符串长度 len 来作为结束标识
使用场景:
SDS 的主要应用在:存储字符串和整型数据、存储 key AOF 缓冲区和用户输入缓冲。
跳跃表是有序集合 sorted-set )的底层实现,效率高,实现简单。
将有序链表中的部分节点分层,每一层都是一个有序链表。
字典dict 又称散列表 hash ),是用来存储键值对的一种数据结构。
Redis 整个数据库是用字典来存储的。( K-V 结构)
Redis 进行 CURD 操作其实就是对字典中的数据进行 CURD 操作。
数组:用来存储数据的容器,采用头指针 + 偏移量的方式能够以 O(1) 的时间复杂度定位到数据所在的内 存地址。
快速列表
快速列表( quicklist )是 Redis 底层重要的数据结构。是列表的底层实现。(在 Redis3.2 之前, Redis 采用双向链表(adlist )和压缩列表( ziplist )实现。)在 Redis3.2 以后结合 adlist ziplist 的优势 Redis 设 计出了quicklist

 

双向链表优势

1. 双向:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为 O(1)
2. 普通链表(单链表):节点类保留下一节点的引用。链表类只保留头节点的引用,只能从头节点插
入删除
3. 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL, 对链表的访问都是以 NULL
束。
4. 带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)
5. 多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
数据压缩
quicklist 每个节点的实际数据存储结构为 ziplist ,这种结构的优势在于节省存储空间。为了进一步降低
ziplist 的存储空间,还可以对 ziplist 进行压缩。 Redis 采用的压缩算法是 LZF 。其基本思想是:数据与前
面重复的记录重复位置及长度,不重复的记录原始数据。

缓存过期和淘汰策略

长期使用, key 会不断增加, Redis 作为缓存使用,物理内存也会满
内存与硬盘交换( swap ) 虚拟内存 ,频繁 IO 性能急剧下降
maxmemory
不设置最大内存的场景:
        key是固定的, 不会增加, 
         Redis 作为 DB 使用,保证数据的完整性,不能淘汰 , 可以做集群,横向扩展
        淘汰策略: 禁止驱除
设置的场景:
        key在不断增加
        默认为0时, 不限制, 直到超过物理内存, 崩溃
设置 maxmemory 后,当趋近 maxmemory 时,通过缓存淘汰策略,从内存中删除对象

expire数据结构

命令:   expire key ttl(单位秒)
数据结构中:  dict存储key-value,    expires则维护了Redis了设置失效时间的key
当我们使用 expire 命令设置一个 key 的失效时间时, Redis 首先到 dict 这个字典表中查找要设置的 key 是 否存在,如果存在就将这个key 和失效时间添加到 expires 这个字典表。
当我们使用 setex 命令向系统插入数据时, Redis 首先将 Key Value 添加到 dict 这个字典表中,然后 将 Key 和失效时间添加到 expires 这个字典表中。
删除策略
Redis 的数据删除有定时删除、惰性删除和主动删除三种方式。
Redis 目前采用惰性删除 + 主动删除的方式。
定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除 操作。
需要创建定时器,而且消耗 CPU ,一般不推荐使用。
惰性删除
key 被访问时如果发现它已经失效,那么就删除它。
调用 expireIfNeeded 函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删 除它。
主动删除
redis.conf 文件中可以配置主动删除策略 , 默认是 no-enviction (不删除)
maxmemory-policy allkeys-lru
LRU
LRU (Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“ 如果数据最近被访问过,那么将来被访问的几率也更高
通过LinkedHashMap来维护缓存数据
1. 新数据插到表头
2. 每当有数据命中, 提到对头
3. 当链表满时, 删除队尾数据
Redis LRU 数据淘汰机制
在服务器配置中保存了 lru 计数器 server.lrulock ,会定时(
redis 定时程序 serverCorn() )更新, server.lrulock 的值是根据 server.unixtime 计算出来的。
另外在RedisObject中有lru的属性, 则每次访问数据, 都会更新
LRU 数据淘汰机制是这样的:在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰。
volatile-lru
从已设置过期时间的数据集( server.db[i].expires )中挑选最近最少使用的数据淘汰
allkeys-lru
从数据集( server.db[i].dict )中挑选最近最少使用的数据淘汰
LFU
LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将 来一段时间内被使用的可能性也很小
volatile-random
从已设置过期时间的数据集( server.db[i].expires )中任意选择数据淘汰
allkeys-random
从数据集( server.db[i].dict )中任意选择数据淘汰
ttl
volatile-ttl
从已设置过期时间的数据集( server.db[i].expires )中挑选将要过期的数据淘汰
redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires
TTL 数据淘汰机制:从过期时间的表中随机挑选几个键值对,取出其中 ttl 最小的键值对淘汰。
noenviction
禁止驱逐数据,不删除 默认
缓存淘汰策略的选择
allkeys-lru : 在不确定时一般采用策略。
volatile-lru : 比 allkeys-lru 性能差 : 过期时间
allkeys-random : 希望请求符合平均分布 ( 每个元素以相同的概率被访问 )
自己控制: volatile-ttl 缓存穿透

你可能感兴趣的:(redis)