redis总结

Redis

使用缓存数据库解决的问题

  • 传统数据库有IO压力,如果采用分库分表的操作,会影响业务的逻辑
  • 负载均衡时,当我们保存session时,要么保存在客户端cookie,会有安全问题,要么将session复制给其他服务器,会有数据冗余问题

NoSQL特点

  • 不支持ACID
  • 性能较高

不适合场景

  • 需要事物支持

Memcache特点

  • 支持简单的key-value,支持类型单一
  • 不能持久化
  • 作为缓存数据库辅助持久化的数据库

Redis特点

  • 覆盖了Memcache的绝大部分功能
  • 数据都在内存,支持持久化,主要用作数据备份
  • 除了key-value,支持多种数据类型的存储,string、list、set、hash、zset

Redis是单线程+多路IO复用技术,有数据库0-15共16个库

key键操作

keys * 查看当前库所有key

exists key 判断某个key是否存在

type key 查看key类型

del key 删除指定的key数据

unlink key 根据value选择非阻塞删除(仅将keys从keyspace元数据中删除,真正的删除会在后续的异步操作)

expire key [过期时间(秒)]

pexpire key [过期时间(毫秒)]

expireat key [在某个时间戳之后过期(精确到秒)]

pexpireat key [在某个时间戳之后过期(精确到毫秒)]

persist key 取消过期时间

ttl key 查看过期时间,-1表示不会过期,-2表示已经过期,正数表示还有多少秒过期

select 切换数据库

dbsize查看当前数据库的key的数量

flushdb 清空当前库

flushall 清空所有库

基本数据结构

string

基本数据类型,一个key对应一个value

二进制安全的,可以存储任何数据,一个字符串value最多为512M

底层数据结构为简单动态字符串(Simple Dynamic String,SDS),字符串长度小于1M时,扩容一倍大小,如果大于1M,扩容一次多1M,最多为512M。

  • 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int

  • 如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstrembstr编码是专门用于保存短字符串的一种优化编码方式

  • 如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw

命令

set key value [EX seconds | PX milliseconds | KEEPTTL] [NX | XX]

  • NX:当数据库中key不存在时,可以将key-value添加数据库

  • XX:当数据库中key存在时,可以将key-value添加数据库

  • EX:key超时秒数

  • PX:key超时毫秒数

    可以缓存整个json对象:SET user:1 '{"name":"xiaolin", "age":18}'

    key值相同时会覆盖

get key

append key value 将给定value添加的key原来value值的末尾,如果不存在,相当于set key

strlen key

setnx key value 只有key不存在才能设置key-value

incr keydecr key 将key中存储的数字加1或者减1,如果为空,新增值为1,原子操作

incrby key incrementdecrby key increment将key中存储的数字加/减 increment长度

mset key1 value1 key2 value2 ...同时设置一个或多个key-value

mget key1 key2 key3 key4...同时获取一个或多个value

msetnx key1 value1 key2 value2同时设置一个或多个key-value对,当且仅当所有给定key都不存在原子性,有一个失败就都失败

setrange key index value 在value的index开始设置value,会覆盖后面

getrange key begin end 获得value的范围[begin, end]

setex key 过期时间 value 设置key-value同时设置过期时间为多少秒,是ex

getset key value 以旧换新,设置新值同时获得旧值

应用

实现分布式锁

利用redis执行lua脚本时,可以原子性的执行,利用set的nx不存在才可以插入,和px设置过期时间,进行实现,解锁时,先判断解锁的用户是否是加锁的客户,是的话才解锁

共享session信息

分布式系统获取session信息的时候先去redis数据库访问

list

单键多值,按照插入顺序排序。底层是一个双向链表,可以通过索引下标找到中间节点,值可以重复。

底层数据结构是quicklist,当列表元素比较小的,会使用一块连续的内存存储,这个结构是ziplist,它将所有元素紧挨着在一起,分配的是一块连续的内存;

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

当列表元素比较多时,会采用quicklist,将多个ziplist连在一起,构成双向链表

在redis3.2之后,list底层数据结构只由quicklist实现,替代双向链表和ziplist

命令

l是左边

lpush/rpush key value1 value2 value3... 从左边/右边插入值 (从左边放,头插法;从右边放,尾插法)

lpop/rpop key 从左边/右边拿出值

rpoplpush key1 key2从key1右边取一个值,插入到key2的左边

l是list

lrange key start end 从左边遍历,没有从右边遍历,0左边第一个,-1右边第一个

lindex key index 按照索引下标获得元素(从左到右)

llen key 列表长度

linsert key before/after value newvalue 在value前面/后面插入newvalue的值,如果有多个相同的值,则插入到第一个前面/后面

lrem key n value从左边删除n个value(从左到右)

lset key index newvalue 将列表key下标为index的值替换为value

应用

消息队列

使用lpushrpop实现消息队列,但是不能通知消费者有新消息写入,这时候需要轮询rpop,所以可以使用brpopBRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据

为了保证唯一性,为每个消息生成一个全局唯一id,利用lpush将这个消息插入list时,需要包含这个全局唯一id

为了保证消息可靠性,可以利用brpoplpush,这个命令让消费者从一个消息队列取出一个消息时,进行备份

set

set跟list类似是一个列表的功能,set可以自动除重,set提供某个成员是否在set集合内的接口。一个集合最多可以存储 231 - 1 个元素。

Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞

底层实现

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。底层是一个value为null的hash表,数据结构是dict字典,字典是用哈希表实现的
命令

sadd key value1 value2 将一个或者多个member元素加入到集合key中,已存在的member被忽略

smembers key 取出该集合的所有值

sismember key value 判断集合key是否有该value值,有为1,没有为0

scard key 返回该集合的元素个数

srem key value1 value2 ... 删除集合中的某个元素

spop key 随机从该集合中弹出一个值

srandmember key n 随机从该集合取出n个值。不会从集合中删除

smove source destination value 把集合中一个value从一个集合移到另一个集合

sinter key1 key2 返回两个以上集合的交集元素

sunion key1 key2 返回两个以上集合的并集元素

sdiff key1 key2 返回两个以上集合的差集元素(key1中的,不包含key2的)

应用场景

点赞:Set 类型可以保证一个用户只能点一个赞

共同好友:Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。

抽奖活动:存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。

hash

hash是一个键值对集合。是一个string类型的field和value的映射表。

全局来看其实就是两层hash,key有一个value,value里有field和value的键值对的映射表

底层数据结构:ziplisthashtable。当field-value长度较短(由hash-max-ziplist-value配置)且个数少(使用hash-max-ziplist-entries配置),使用ziplist,否则使用hashtable

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

命令

hset key field1 value1 field2 value2... 给key 集合中的field键赋值value

hget key field 从key集合field键中取出value,只能取一个

hmset key field1 value1 field2 value2... 给key 集合中批量field键赋值value

hmget key filed1 filed2... 批量从key集合中field取出value

hexists key field 查看哈希表key中,给定域field是否存在

hkeys key 列出该hash集合的所有field

hvals key 列出该hash集合的所有value

hincrby key field increment 为哈希表key中的域field的值加上增量

hsetnx key field value 给key 集合中的field键赋值value,当且仅当域field不存在

应用场景

缓存对象

购物车:以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素

Zset

有序集合Zset和无序集合set类似,都是一个没有重复元素的字符串集合

有序集合zset的每个成员都关联了一个评分(score),这个评分被用来按照最低到最高进行排序集合的成员。集合成员是唯一的,但是评分可以重复

底层使用了两个数据结构

  • ziplist,第一个节点保存value,第二个节点保存score。ziplist的集合元素按照从小到大排序
  • skiplist,实际上redis对字典和跳表进行了封装。按照从小到大保存所有元素。通过这个跳表,程序可以对有序集合进行范围操作,如zrange和zrank等命令基于跳表实现
命令

zadd key score1 value1 score2 value2 将一个或多个member元素及其score值加入到有序集合key中

zrange key start stop [WITHSCORES] 返回有序集合key中,下标在[start, stop]之间的元素,带WITHSCORES,可以让分数和value一直返回结果集

zrangebyscore key min max [WITHSCORES][LIMIT OFFSET COUNT] 返回有序集合key中,所有score值介于[min, max]之间的成员。按照score值递增排序。offset表示从第几个开始(0开始计数),选取count个

zrerangebyscore key max min [WITHSCORES][LIMIT OFFSET COUNT] 同上,改为从大到小排序

ZRANGEBYLEX key min max [LIMIT offset count] 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。

zincrby key increment value 为元素的score加上增量

zrem key value 删除该集合下,指定值的元素

zcount key min max 统计[min, max]分数区间内的元素个数

zrank key value 返回该值在集合中的排名,从0开始

应用场景

排行榜

电话、名字排序

新的数据类型

Bitmap

Bitmap,即位图,是一串连续的二进制数组(0 和 1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

命令

setbit key offset value 设置Bitmaps中某个偏移量的值(0和1) offset从0开始

getbit key offset 获取Bitmaps中某个偏移量的值

bitcount key [start end] 统计字符串从start字节到end字节比特值为1的数量,[start, end]以字节为单位

BITOP [operations] [result] [key1] [keyn…] 将key1, key2, … 进行operations(and/or/not/xor),结果保存在result

HyperLogLog

每个HyperLogLog键只需要花费12kb,就可以计算接近 264个不同元素的基数。HyperLogLog只会根据输入元素来计算基数,但是不会存储输入元素,所以不能返回输入的各个元素.基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。

  • 可用于计算UV(页面独立访问数)
命令

pfadd key element [element...] 将元素添加到指定的HyperLogLog数据结构中。如果执行后HLL的近似基数发生变化,则返回1,否则返回0

pfcount key [key...] 返回给定 HyperLogLog 的基数估算值。

pfmerge destkey sourcekey [sourcekey...] 将多个 HyperLogLog 合并为一个 HyperLogLog,并且保存在destkey中

Geospatial

Geospatial 主要用于存储地理位置信息,并对存储的信息进行操作

GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

命令

geoadd key longitude latitude member [longitude latitude member ...] 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。有效经度从-180到180度,纬度从-85.05112878到85.05112878度。超出范围会报错。已经添加的无法再次添加

geopos key member [member ...] 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。

geodist key member1 member2 [m|km|ft|mi] 返回两个给定位置之间的距离。

georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

Stream

Stream 类型用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠

命令
  • XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
  • XLEN :查询消息长度;
  • XREAD:用于读取消息,可以按 ID 读取数据;
  • XDEL : 根据消息 ID 删除消息;
  • DEL :删除整个 Stream;
  • XRANGE :读取区间消息
  • XREADGROUP:按消费组形式读取消息;
  • XPENDING 和 XACK:
    • XPENDING 命令可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;
    • XACK 命令用于向消息队列确认消息处理已完成;

消费组:

可以使用xgroup create创建消费组,当使用xadd时,会向多个group发送消息,消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息。但是,不同消费组的消费者可以消费同一条消息(但是有前提条件,创建消息组的时候,不同消费组指定了相同位置开始读取消息)

Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。

消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成

  • Redis 生产者会不会丢消息?生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。 从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 ( MQ 中间件) 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,如果返回异常则进行消息重发,那么这个阶段是不会出现消息丢失的。

  • Redis 消费者会不会丢消息?不会,因为 Stream ( MQ 中间件)会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,但是未被确认的消息。消费者可以在重启后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。等到消费者执行完业务逻辑后,再发送消费确认 XACK 命令,也能保证消息的不丢失。

  • Redis 消息中间件会不会丢消息?

    会,Redis 在以下 2 个场景下,都会导致数据丢失:

    • AOF 持久化配置为每秒写盘,但这个写盘过程是异步的,Redis 宕机时会存在数据丢失的可能
    • 主从复制也是异步的,[主从切换时,也存在丢失数据的可能 (opens new window)。

发布和订阅

发布者可以往多个频道(channel)发布消息,订阅者只能订阅一个频道,当发布者向频道发布消息时,订阅者可以收到消息

订阅 subscribe channel

发布 publish channel message

事务

redis可以开启事务,但是不保证原子性。redis事务的本质:一组命令的集合。一个事务中的命令都会被序列化,在事务执行过程中会按照顺序执行。一次性,顺序性,排他性(执行过程不允许被别人干扰)

在开启事务后,所有的命令会先入队,然后当发起执行命令的时候才真正执行

  • 开启事务 multi
  • 执行事务 exec
  • 回滚/取消事务 discard
  • 监视 watch keyWATCH 本身的作用是监视 key 是否被改动过,而且支持同时监视多个 key,只要还没真正触发事务,WATCH 都会尽职尽责的监视,一旦发现某个 key 被修改了,在执行 EXEC 时就会返回 nil表示事务无法触发
  • 放弃监视 unwatch,当原来的乐观锁失效时,需要先unwatch解锁再watch获取最新的值
redis事务错误
  1. 调用 EXEC 之前的错误

    ​ 有可能是由于语法有误导致的,也可能时由于内存不足导致的。只要出现某个命令无法成功写入缓冲队列的情况,那么整个事务都不会被执行

  2. 调用EXEC之后的错误

    ​ 对于入队成功的语句,即使有错误,也不会影响其他正确语句的执行。所以redis的事务不是原子性

redis实现乐观锁
线程1线程2
set k1 10
watch k1 //获取最新的值
multi
incr by k1 100
set k1 20
get k1 //k1为20
exec
//对比监视的值是否为原来的值,如果是则成功;如果不是则返回nil,表示事务失败

持久化

AOF日志

当redis执行一个写命令之后,会把命令以追加的形式写到AOF日志中

这样有两个好处

  • 避免了额外的检查,当先写AOF日志再进行写操作,如果写操作有错误的时候,如果没有进行检查的话,那么AOF里的命令就是错误的
  • 不会阻塞当前命令的执行,这里会有两个隐患
    • 由于redis是单线程的,写日志和执行命令是在一个线程里,这样当IO压力比较大的时候,下一个命令会阻塞
    • 由于是写操作和写日志是两步,所以当写操作完成之后,命令还没存到日志中就宕机,存在数据丢失的风险
三种写回机制

Redis每次执行写操作之后,就会把命令写入到server.aof_buf中,然后调用write将server.aof_buf中的内容写入到page Cache中的AOF文件,具体什么时候将page Cache中的AOF文件持久化到磁盘中,由内核决定

  • Always,每次执行完一条写命令,持久化到磁盘中的AOF文件。同步
  • Seconds,每次执行完一条写命令,先将命令写入到内核缓冲区的AOF中,然后每秒再将内核缓冲区中的AOF文件持久化到硬盘中。异步
  • No,每次执行完一条写命令,就先将命令写入到内核缓冲区的AOF文件中,等待操作系统决定什么时候将内核缓冲区的内容自动持久化到硬盘中

Always可以最大程度保证数据不丢失,但是会影响主进程性能

No相比Always性能更好,但是如果AOF文件没有写入到磁盘中,由于不知道操作系统什么时候将内核缓冲区持久化到磁盘中,一旦服务器宕机,则会导致数据丢失

Seconds就是这两者折中,当然如果上一秒没有写操作命令没有写入到磁盘中,也会导致数据丢失

如果想要应用程序向文件写入数据后,能立马将数据同步到硬盘,就可以调用 fsync() 函数,这样内核就会将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。

  • Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数;
  • Everysec 策略就会创建一个异步任务来执行 fsync() 函数;
  • No 策略就是永不执行 fsync() 函数
AOF重写机制

当AOF文件比较大的时候,为了避免性能问题,会进行重写。重写的方式是读取数据库中所有的键值对,然后将每一对键值对一条命令记录到新的AOF文件中。重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对

使用两个AOF文件的原因是避免当AOF重写失败时,如果使用一个AOF文件,则会污染原来的AOF文件,无法用于恢复使用

AOF后台重写

Redis的重写AOF过程是由后台进程bgrewriteaof完成的

在AOF重写之前,主进程会进行fork创建出后台子进程,此时主进程和子进程的页表是一样的,页表对应的页表项会标记该物理页为只读,当主进程或子进程读取物理内存的时候,此时父子进程共享物理内存,当主进程或者子进程对数据进行写操作的时候,会触发写时复制**,也就是cpu会触发缺页中断,然后操作系统会在缺页异常处理函数中将物理内存进行复制,设置映射关系和读写权限,最后才对物理页进行写操作。

所以会有两个阶段阻塞父进程

  • fork进程的时候,会将页表复制给子进程,阻塞的时间由页表的大小决定
  • 写时复制的时候,会进行缺页中断,对物理内存进行复制,同时建立映射关系,这个时候如果物理页比较大,那么就会阻塞比较久

子进程在进行AOF重写操作的时候,主进程可以正常处理命令

如果在AOF复制的时候,父进程对键值对进行修改的时候,会导致数据不一致的问题。

为了解决这个问题,redis设置了AOF重写缓冲区。子进程进行AOF复制的时候,父进程在完成写操作之后,会将命令放到AOF缓冲区和AOF重写缓冲区中,当子进程完成AOF复制之后,会向主进程发送一条信号,主要将AOF重写缓冲区里的内容追加到AOF文件中,然后改名替换旧的AOF文件,信号处理函数执行完之后主进程就可以继续执行命令了

RDB快照

RDB快照保存的是某一瞬间的内存数据,记录的是实际数据,进行恢复数据的时候,直接将RDB文件读入内存就可以了,而AOF日志保存的是命令操作,恢复数据的时候需要执行额外的命令。

Redis提供了两个命令来对RDB快照进行保存,savebsavesave是在主进程生成RDB文件,bsave是会创建出后台进程后,在后台进程生成RDB文件。当使用bsave的时候,也是会通过写时复制的方式来拷贝。RDB快照的加载是会在启动服务器的时候自动执行,所以没有加载RDB快照的命令。

触发机制

  • save的规则满足情况下
  • 执行flushall命令
  • 退出Redis

当使用bsave生成RDB快照时,会有几个问题

  1. 当主进程对数据进行修改,由于是写时复制,主进程会拷贝新的物理内存,子进程只能看到原来的物理内存,保存的是旧的物理内存,如果系统在RDB文件生成之后崩溃了,那么主进程修改的数据也就丢失了。
  2. 由于写时复制,如果主进程修改了数据,那么当前修改的数据就会被拷贝,极端情况下,物理内存会被拷贝两份

尽管RDB快照比AOF文件恢复速度快,那么如果RDB生成 频率比较低,当系统崩溃时会丢失比较多的数据,而如果生成频率比较高,又会影响主进程的性能。

所以在Redis4.0之后,RDB和AOF进行混合,叫做混合使用AOF日志和内存快照。在AOF在保存数据的时,fork出来的重写子进程会先将与主进程共享的物理内存RDB方式写入AOF文件,如果在此期间主进程进行修改,那么会保存到重写缓冲区中,AOF重写缓冲区的内容以AOF的形式(发送信号,让主进程将重写缓冲区的内容写入AOF文件)写入AOF文件,写入完成后通知主进程将含有RDB格式和AOF格式的AOF文件替换。

过期删除策略和内存淘汰策略

过期删除策略

对一个key设置过期时间时,会将key和过期时间存储到Redis的过期字典中,这个过期字典在redisDb结构中,过期字典实际上是一个哈希表。

过期字典的key是一个指针,指向某个键对象,而过期字典的value是一个long long类型的值,存储key的过期时间。

当我们判断一个键是否过期,是查看过期字典中是否存在该键,如果不存在,则正常取键值,如果存在,则将过期时间与当前时间进行对比,如果过期时间比当前时间大,那么过期,否则不过期,正常取值

过期删除策略

  • 定时删除,在设置key的过期时间时,同时创建一个定时器时间,当时间到达时,自动由定时器删除该key

    • 好处:可以保证过期的key会尽快被删除,减少内存资源
    • 坏处:增加cpu的压力,当cpu比较繁忙时,会消耗cpu在删除与当前任务无关的过期键上
  • 惰性删除,每次使用到某个key时,先检查这个key是否过期,如果过期则把它删除掉,如果不过期则返回值

    • 好处:减少cpu资源
    • 坏处:会增加内存资源
  • 定期删除,使用一个定时器,当定时器到的时候,随机抽取一些key,并且检查这些key是否过期,如果过期则删除掉

    • 介于惰性删除和定时删除之间

Redis采用惰性删除和定期删除策略相结合

Redis惰性删除的策略是,当访问key时,先调用expireIfNeeded 检查key是否过期,如果过期则选择把它同步删除掉还是异步删除掉,如果没有过期则返回正常的键值对。

Redis定时删除的策略是,每秒进行10次过期删除数据库,这个次数由redis.conf中的hz进行配置,删除的方式是从过期字典中随机抽取20个key,检查这而20个key是否过期,如果过期了则把它删除掉,并且如果删除的个数超过1/4,也就是5个,那么就会再次进行定时删除,定时删除的循环时间默认不超过25ms

内存淘汰策略

  • 不进行数据淘汰策略

    • noeviction(Redis3.0之后默认的内存淘汰策略),它表示当内存达到最小限度的时候,不进行内存淘汰,等到进行写数据操作的时候,再触发oom,但是如果没有数据写入,只是进行读操作和删除数据,则照常进行
  • 进行数据淘汰策略

    • 根据过期时间的数据
      • volatile-random:随机淘汰设置了过期时间的键值
      • volatile-ttl:优先淘汰更早过期时间的键值
      • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值
      • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最近最少使用的键值
    • 全部数据
      • allkeys-random:在全部数据中随机选择几个淘汰
      • allkeys-lru:淘汰所有键值中最近最久未使用的键值
      • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有键值中最近最少使用的键值

LRU算法

传统LRU算法是根据哈希表和链表实现的,链表按照访问顺序排序,在链表前面是最近被访问的,在链表后面是最近最少被使用的,会淘汰链表后面的数据

Redis的LRU算法

Redis实现的是近似LRU算法,在数据结构中使用了一个访问时间的额外字段lru,该字段表示当前数据的最近的访问时间,然后在进行lru算法的时候,使用随机采样的方式来进行淘汰,它是随机选择5个(可以配置),然后淘汰最久没有使用的那个

LRU算法缓存问题,当读取了大量的数据之后,由于这些数据大概率只会被读取一次,这些数据会存放在缓存很长一段时间,从而造成了缓存污染

LFU算法

LFU算法是最近最少被使用,LFU会基于每个数据的访问次数,对访问次数最少的数据进行淘汰。

Redis的LFU算法

Redis使用了lru的字段,前面几个字节代表最近的访问时间,后面几个字节代表数据的访问频率。当访问了数据之后,会基于访问时间减少访问频率,然后再根据一定概率增加访问频率,如果两次访问时间相隔比较长时间,那么访问频率就会被减少的比较多,系统会优先淘汰访问频率最少的数据。

主从复制

Redis使用了主从复制,主服务器可以提供读写操作,而从服务器只能使用读操作,当主服务器执行了写操作,会同步给从服务器

命令:replicaof host port 建立主从关系,使用replicaof no one 取消主从关系

​ psync masterid offset 请求数据同步

​ fullresync

当从服务器第一次同步主服务器时,会做如下操作

  1. 从服务器使用replicaof命令选择主服务器,建立主从关系,之后会发送psync命令表示要进行数据同步。主服务器接收psync命令之后会发送FULLRESYNC命令给从服务器作为回复,表示全量复制,会将主服务器的全部数据同步给从服务器
  2. 主服务器使用bsave命令生成RDB文件,然后发送给从服务器,从服务器接收到RDB文件之后会先清空当前数据,然后加载RDB文件。在此期间,如果主服务器使用了写操作,那么会存放在replication_buffer中。replication_buffer会在几个时间段加载数据
    1. 主服务器生成RDB文件
    2. 主服务器发送RDB文件
    3. 从服务器加载RDB文件
  3. 从服务器执行完RDB文件之后,会发送命令表示RDB文件加载完毕,之后主服务器会把replication_buffer中的内容发送给从服务器。从服务器接收并执行replication_buffer里的内容。至此,主从服务器就同步完毕

第一次数据同步之后,主从之间就维持一条TCP的长连接

为了避免过多的从服务器跟主服务器进行全量复制,造成主服务器忙于fork创建子进程,阻塞主进程,从而让Redis无法正常处理请求,还有由于传输RDB文件会占用带宽,延迟主服务器的响应,可以采用让从服务器充当“经理”的身份,让其他从服务器连接这个从服务器,这个从服务器给其他从服务器进行数据同步

如果主从服务器在进行数据同步的期间断开了连接,之后再次连接上,主从服务器数据就不同步了。

在Redis2.6之前,主从服务器在重新连接上之后,主服务器会进行全量复制给从服务器用于数据同步,但是全量复制会占据资源。

在Redis2.6之后,主从服务器在重新连接上之后,主服务器会进行增量复制给从服务器。主服务器会将断开连接之后的命令保存在repl_backlog_buref环形缓冲区中,主从服务器用一个偏移量来代表自己的在缓冲区的位置。

主从服务器进行增量复制的方式是,从服务器重新连接之后,会发送psync命令将自己的偏移量给主服务器。主服务器收到命令后,回复continue命令表示进行增量复制,再根据自己的位置和从服务器的位置决定是全量复制还是增量复制

  1. 如果从服务器想读的数据已经被覆盖了,主服务器就会采用全量复制的方式进行数据同步。
  2. 如果从服务器想读的数据在repl_backlog_buffer中,那么主服务器就会采取增量复制的方式进行数据同步。

如果是增量复制,会将repl_backlog_buffer中的命令加到replication_buffer中,再将replication_buffer发送给从服务器执行。

为了避免网络恢复时,主服务器频繁使用全量复制,应该适当调大repl_backlog_buffer的大小

哨兵模式

在Redis的主从架构中,由于主从读写分离,当主节点挂了之后,那么就没办法响应客户端的写请求,也没办法给从节点同步了。这个时候如果要回复,需要人工介入,重新选择新的主节点,将旧的主节点的从节点指向新的主节点,还要通知上游连接旧主节点的客户端,将配置中的主节点ip地址改为新的主节点。

Redis设置了哨兵模式,哨兵是运行在特殊模式下的Redis进程,会观察主从节点,主要负责观察,选举,通知三件事。

哨兵会持续监控主节点和所有从节点,每隔1秒会给主从节点发送ping命令,如果主从节点在规定时间内没有回复命令,那么哨兵就判断主从节点主观下线。当哨兵节点判断主节点主观下线后,就会在哨兵集群中进行投票判断主节点是否客观下线,当投票数量超过哨兵配置文件中的quorum后就会被该哨兵节点判断为客观下线。之后在哨兵集群中选定一个哨兵作为leader进行主从故障转移。

哨兵集群会选择发现主节点主观下线的哨兵节点作为候选者,当有多个哨兵发现主节点主观下线,就会一起充当候选者,哨兵集群中的每个哨兵都只有一票来选择哪个哨兵成为leader,只有候选者可以投自己,其他哨兵要投票选择候选者,当某个候选者的投票数量超过总哨兵节点数的一半并且超过配置文件中quorum的数量,才会被选择成为leader。

leader会在从节点中选举出一个新的主节点,选择的方式是在从节点中选择一个按照网络状态良好,优先级最小,复制进度最大的,从节点id最小的顺序选择一个从节点作为新的主节点。leader通过向从节点发送replicaof no one命令使其成为主节点。

之后leader将旧主节点的所有从节点指向新的主节点,同时通知客户端,当旧的主节点重新连接之后,哨兵会重新将旧主节点设置为新主节点的从节点。

  1. 选出新的主节点
    1. 首先把已经下线的从节点排除掉,然后把网络状态不好的从节点排除掉
      1. 网络状态不好是通过down-after-millseconds * 10 的配置项,其down-after-millseconds是主从节点断连的最大超时时间。如果在down-after-millseconds时间内都没有连接上,那么就说明主从节点断连了。如果断连次数超过10次,那么就说明从节点网络状态不好。
    2. 然后根据优先级进行排序,在从节点的配置文件中可以给从节点设置优先级
    3. 如果优先级一样,那么就会根据复制进度进行排序,从节点复制得比较多的会优先成为主节点
    4. 如果优先级和复制进度一样,那么最后就将从节点id比较小的那个成为主节点
  2. 将从节点指向新的主节点
    1. 哨兵leader会向所有从节点发送replicaof命令,让它们成为新的主节点的从节点
  3. 通知客户端
    1. 哨兵通知客户端的方式是通过订阅者发布者模式。哨兵节点完成主从切换之后就会通过+switch master频道通知给客户端,客户端收到频道消息得到新的主节点的ip地址和端口号信息,之后就可以与新的主节点进行通信
  4. 继续监视旧的主节点
    1. 当旧的主节点重新上线后,哨兵节点就会发送replicaof命令让其成为新的主节点的从节点

哨兵集群是怎么互相发现的

配置哨兵的消息是通过sentinel monitor <master-name> <ip> <redis-port> <quorum>进行配置的

哨兵集群同样也是通过发布者订阅者模式进行的。在主节点上有一个__sentinel__:hello的频道,哨兵节点配置上主节点之后,就会将自己的ip地址和端口号发到频道上,其他哨兵就可以通过这个频道收到这个哨兵的地址和端口,然后就可以与这个哨兵建立连接了

哨兵与从节点进行连接

主节点上有从节点的消息,哨兵每隔10秒会通过info命令获取主节点上的从节点信息,主节点接到命令后就会把从节点列表发给哨兵节点,哨兵之后就根据从节点列表来与每个从节点建立连接,并在这个连接上持续对从节点进行监控。

切片集群

在只有一个主节点多个从节点的主从架构下,有几个问题

  1. 由于写操作只能经过主节点,那么在海量数据高并发的场景下,一个节点的写操作压力会比较大,会出现瓶颈
  2. 由于只有一台主节点进行读写,而其他从节点作为主节点的热拷贝而不进行写操作,所以本质上只有主节点作为存储,如果面对海量数据的存储,那么一台主节点是存储不过来的,而且数据量太大会导致持久化成本太高,严重时会阻塞服务器。

Redis的集群模式,将数据进行分片,将不同的数据分配到不同的主节点上。并且,Redis的集群模式采用无中心化的思想,对于客户端来说,可以将整个集群看作一个整体,可以连接任意一个节点进行操作,就像连接只有一个节点一样。Redis集群也支持高可用模式,每个主节点都有多个从节点作为备份。

如何将数据存储到不同的节点上

常见的分区算法有:hash算法,一致性hash算法, hash槽分区算法

采用hash算法会导致加减节点的时候需要对数据进行重新映射,最坏的情况下需要对所有数据重新映射

而一致性hash算法虽然解决了hash算法加减节点时会影响所有节点,只会影响哈希环上顺时针的下一个节点。但是因为一致性hash算法不能保证节点都均匀分布在hash环上,当大量数据请求一个节点,这个节点容易崩溃,崩溃之后下一个节点容易受到影响,造成雪崩的连环效应

Redis的哈希槽分区算法

Redis集群有16384个哈希槽,将不同的hash槽分配到不同的Reids节点上进行管理,每个节点只负责一部分哈希槽。对数据进行操作的时候,集群会对key使用CRC16算法运算并且模上哈希槽的个数(slot=CRC(Key)%16384),得到的结果放到对应的槽上。如果查找值,可以根据key算出对应在哪个槽上,就可以找到对应的Redis节点。

哈希槽的好处就是可以方便的添加和删除节点,当添加节点的时候,只需要把其他节点的某些槽移动到这个节点上。当删除节点时,只需要把删除节点的槽移动到其他节点上就可以了。

默认情况下,集群的slave是不提供读写操作的,因为集群的核心理念,主要是slave是做主节点的热备份以及主节点故障时的主备切换。而Redis的读写分离,是为了横向扩展从节点来提高高并发下的读操作。Redis的集群是通过横向扩展主节点来提高高并发下的读写操作的。

Moved请求和ASK请求

既然Redis的数据是分布在不同的主节点上的,那么是怎么通过一个客户端来获取所有的数据的

MOVED请求

Redis的客户端通过CRC16算法对key进行计算并且取余16384(CRC16(key) % 16384)得到key是在哪个槽(slot)上的,根据槽位跟节点的映射信息找到这个节点,然后就会去访问这个节点,当这个节点有当前数据的时候就会返回结果,如果因为这个槽(slot被移动到别的节点之类的原因,客户端就无法从这个节点获取数据。但是每个主节点保存了集群中所有主节点信息,主节点就会返回MOVE重定向请求和新的IP地址以及端口号告诉客户端数据被移动到哪个主节点上,然后客户端根据新的IP地址和端口号访问对应的节点取得数据。

ASK请求

如果两个节点正在做数据迁移的操作,这个时候客户端来访问主节点得到数据,如果这个数据在客户端访问的主节点上,那么这个主节点就会返回对应的数据;而如果这个数据对应的槽已经被移动到另外一个节点上,主节点就会返回ASK重定向请求以及对应节点的IP地址和端口号,客户端根据新的IP地址和端口号发送asking命令去对应的节点取得消息,另一个节点返回数据是否存在该节点上的结果。

smart客户端

大部分情况下,客户端可能会出现一次重定向才能找到正确的节点,这无疑会增加集群的网络负担和单次请求耗时。所以大部分的客户端都是smart的,在本地会缓存一份哈希槽(hashslot)和集群中的节点的映射表,大部分情况下直接走本地缓存就可以找到对应的节点,而不需要走moved和ask重定向。

MOVED和ASK请求的区别

ASK重定向请求是集群正在进行slots数据迁移,客户端并不知道什么时候会结束,因此只是临时性的重定向,所以不会更新本地缓存;而MOVED重定向请求是键对应的槽已经明确在某个节点上,客户端会更新本地slots缓存

集群同步

当有节点加入、删除、slot移动、主备切换,Redis不同节点是如何通信维护集群同步的?

在Redis集群中,是不同节点通过gossip协议来进行通信,节点通过互相交换元数据来达到最终一致性,元数据就是指节点包含那些数据,是否有故障之类的。

gossip的原理,就是通过不同节点之间不停互相交换信息,最终使每个节点都有了整个集群的信息。每个节点可能知道所有其他节点,也可能只知道邻居几个节点,但只要这些节点网络是联通的,最终他们的状态就会一致。

gossip协议的好处,就是即使集群节点的数量增加,每个节点的负载也不会增加很多。但是缺点是元数据的更新有延时。

gossip协议对网络和时间戳要求比较高,如果时间戳有错误可能会影响数据的有效性。当节点数量比较多的时候,最终一致性的时间相对会比较长,官方推荐最大节点是1000个左右。

redis cluster架构下要求每个节点都有两个端口号,一个用来跟客户端通信,一个用来节点之间进行通信(端口号加1w)

gossip协议的通信过程

  1. 集群中每个节点之间都会建立一条tcp连接
  2. 在固定的周期内随机选择几个节点发送ping通信
  3. 收到ping的节点回复pong作为相应。

gossip协议的消息类型:

  1. meet:将新节点加入到集群之中,通过cluster meet ip port命令,已有集群会向新节点发出邀请,加入现有集群
  2. ping:用于交换节点的元数据。每个节点每秒会向集群中其他节点发送消息,消息中封装了自身的消息还有其他部分节点的元数据消息,比如节点管理的槽信息。
    1. ping命令会携带一些元数据,如果比较频繁,会加重网络负担。每个节点每秒会发送10次ping,每次会选择5个最久没有发送的
    2. 如果发现某个节点超过cluster_node_time / 2没有进行通信,那么立刻发送ping,避免交换时间过长造成数据滞后
    3. 每次ping除了会带上自己的消息,还会带上1/10其他节点的消息发送出去。最少包含3个其他节点的消息,最多包含(总结点数 - 2)的消息
  3. pong:用于meet和ping消息的回应,同样也有自身节点的消息还有其他部分节点的元数据消息。
  4. fail:某个节点发现另一个节点fail之后,会向集群中其他节点广播该节点挂掉的消息,其他节点收到后标记其下线

redis集群伸缩在于数据和槽在节点之间移动

扩容节点

  1. 启动新节点
  2. 使用cluster meet命令将新节点加入到集群中
    1. 旧节点会为新节点创建一个Cluster Node结构,并将其加入到自己的ClusterState.nodes字典中
    2. 旧节点根据cluster meet命令给定的ip地址和端口号,向新节点发送meet消息
    3. 新节点收到旧节点的meet消息后,为旧节点创建一个Cluster Node结构,并将其加入到自己的Cluster.nodes字典中,并且回复pong命令
    4. 旧节点收到pong命令之后,就知道新节点已经收到meet消息。之后会发送ping消息。
    5. 新节点收到ping消息之后,就知道旧节点收到了回复的pong消息。
    6. 之后旧节点会将新节点的消息广播给集群中其他节点。其他节点再与新节点建立连接,最后得到最终一致
  3. 迁移槽和数据:添加新节点后,需要将一部分数据和槽分给新节点
    1. 客户端发送命令通知目标节点准备导入槽数据cluster setslot slot importing sourceNodeId
    2. 之后发送命令给源节点,让其准备迁出对应的槽数据cluster setslot slot migrating targetNodeId
    3. 源节点准备好迁移数据了,在迁移数据之前要把数据取出来cluster getkeysinslot slot count
    4. 之后在源节点上把数据迁移到目标节点上migrate targetIp targetPort timeout keys
    5. 重复取出和迁移数据到目标节点。
    6. 完成数据迁移到目标节点之后,就通知对应的槽被分配到目标节点上,并且广播这个消息给其他节点,完成自身槽节点映射表的更新。cluster setslot slot node targetNodeId

收缩节点。

  1. 迁移槽节点
  2. 通知其他节点忘记本节点

为了安全删除节点,Redis只能下线没有数据的节点。如果下线有负责槽数据的节点,只能先把槽数据迁移到其他节点上,再进行删除。下线的时候需要通知全网忘记自己。

集群故障检测

如果一个节点发现另外一个节点的通信时间超过了cluster-node-timeout,那么就会标记为客观下线

当一半以上的主节点标记一个节点为客观下线时,就会触发客观下线的流程(只针对主节点)。将主观下线的节点加入到本地ClusterNode的fail_reports链表中,并且对主观下线的节点进行检查,当超过cluster_node_timeout * 2时,就忽略这个报告,否则标记为客观下线。接着向集群中所有节点广播一条Fail消息,所有收到消息的节点都会标记主管下线的节点为客观下线。

故障恢复

每个从节点会定期检测主节点是否客观下线,然后选出一个从节点进行替代,选举过程是基于Raft协议选举的

  1. 排除网络不好的从节点,如果超过cluster-node-timeout * cluster-slave-validity-factor,那么就判断网络不好
  2. 集群投票选举
    1. 将剩下的从节点按照优先级、复制进度、从节点id进行排序,然后选出某个从节点
    2. 从节点更新配置纪元
      1. 每个主节点会取更新配置纪元,这个值记录了每个节点的版本跟整个集群的版本
    3. 发起选举
      1. 更新完配置纪元后,被选出的从节点向集群发出投票选举的消息,要求收到消息并且具有选举权的主节点进行投票,每个从节点只能再一个纪元中发起投票
    4. 选举投票
      1. 如果一个主节点具有投票权,并且没有投给其他从节点,那么就会向从节点投出一票。当投给某个从节点的数量超过全部主节点数量的一半 + 1时,就会被选举出主节点。如果当前这一轮没有投出,本轮投票没有选出主节点,那么本轮投票作废,更新配置纪元,进行下一轮选举,直到选出主节点
  3. 替换主节点
    1. 当从节点被选为主节点之后,就会触发替换主节点的操作。将旧主节点的槽数据移动到自己节点上,然后广播通知集群中其他节点
      1. 被选出的主节点执行replicaof no one,使自己成为主节点
      2. 新主节点将所有指向旧主节点的槽指向自己
      3. 然后向集群中所有节点广播pong消息,告诉其他节点自己已经成为主节点
      4. 新的主节点开始进行正常操作

如果集群中某个master和slave全部都下线,那么集群就会进入fail状态,不可用。

集群脑裂

在Redis的主从架构种,部署方式一般是一主多从,主节点提供写操作,从节点提供读操作。如果此时主节点的网络发生了问题,它与所有从节点都发生了失联,但是与客户端的网络是正常的,此时客户端并不知道Redis内部已经发生了问题,继续向主节点发送写数据操作,主节点缓存了客户端的数据,但是网络问题无法同步给从节点。此时,哨兵发现主节点失联,于是重新选举出一个主节点,此时就出现了脑裂现象,也就是一个Redis有两个主节点。

然后主节点的网络又好了,**此时旧主节点就会成为新主节点的从节点,然后向新主节点请求数据同步,因为第一次数据同步,于是会清空自己本地的数据,再做全量复制。**所以,此前客户端发送过来的写操作数据就丢失了,这就是集群脑裂造成的数据丢失。

解决办法

当主节点发现从节点下线或者通信超时的总数量超过阈值时,就禁止主节点进行写数据,直接把错误返回给客户端

通过min-slaves-to-write x,主节点必须至少要有x个从节点连接,如果小于这个数,主节点就会禁止写数据

通过min-slaves-max-lag x,主从数据复制和同步的延时不能超过x秒,如果超过,主节点会禁止写数据

两个配置项组合后的要求是,主节点至少要有n个从节点连接,并且和主从复制的ACK消息延时不能超过m秒,否则,主库就不会接收客户端的写请求了

即使主节点假故障,它也没办法在假故障期间和从节点与哨兵通信,自然也没办法进行ACK确认了。这样,min-slaves-to-write和min-slaves-max-lag的要求也就没办法达到,原主节点就会被限制客户端写请求了,客户端也没办法在主节点中写入数据。

等到新主节点重新上线后,就只有新主节点能够与客户端通信,此时,新写入的数据会被写入到新主节点中,而原来的主节点会被哨兵降级为从节点,即使它的数据被清空了,也不会有新数据丢失

数据库和缓存如何保证一致性

无论先更新数据库还是先更新缓存,如果同时对同一条数据多次并发的操作,会有并发的问题,造成数据库与缓存不一致。

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

例如:A先更新数据库的key为1,同时B更新数据库的key为2,然后B更新缓存key为2,最后A更新缓存key为1。

这样数据库中key的值为2,但是缓存key为1,数据库跟缓存就不一致了

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

例如:A先更新缓存的key为1,同时B更新缓存的key为2,然后B更新数据库的key为2,最后A更新数据库key为1。这样数据库中key为1,但是缓存key为2,数据库跟缓存不一致

利用缓存读取不到会去数据库进行更新的特点,可以把缓存删除,等到真正读取数据的时候,发现缓存没有命中,再去数据库读取数据,更新缓存。这种叫做Cache Aside策略

但是无论先更新数据库再删除缓存,还是先删除缓存再更新数据库,同样也有数据不一致的问题。

不过利用缓存比磁盘IO快的原理,发生数据不一致的问题几率会比较小。

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

如果先删除缓存,再更新数据库。有读写并发问题

例如:A发现缓存没有命中,然后去数据库读取数据key为1,同时B要更新数据,先删除缓存,再更新数据库key为2。最后A再把从数据库读取的数据更新到缓存中,key为1。缓存key为1,数据库key为2,这样就出现了缓存不一致问题。

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

如果先更新数据库,再更新缓存,也有读写并发问题

例如:A读取缓存发现缓存没有命中,然后去数据库读取数据key为1,同时B先更新数据库key为2,然后再把缓存删除,最后A把之前从数据库读取到的数据key为1用来更新缓存。数据库key为2,缓存key为1,这样就出现了读写不一致的问题。

通常,操作内存比操作IO要快,这样,A大概率在B删除缓存之前就把缓存更新了,然后B再把缓存删除,就可以得到数据一致性。

解决办法

对于删除缓存的策略来说,会导致缓存命中失败,对于缓存命中率有比较高要求的业务来说,则使用更新数据库+更新缓存的策略。

对于无论先更新数据库还是先更新缓存的策略,有两种解决办法

  1. 可以使用分布式锁,每次只允许一个线程进行数据的更新,就可以保证数据的一致性,但是由于加锁,于是会损耗性能。
  2. 在更新完缓存之后,设置一个较短的过期时间,这样即使导致数据不一致,缓存的数据也会很快过期

对于更新数据库+删除缓存的策略来说,使用延迟双删的策略

延迟双删

先删除缓存数据,然后再更新数据库,之后睡眠(sleep)一段时间,最后再删除缓存数据

睡眠一段时间的目的是为了并发读取缓存之后发现缓存不存在更新缓存,在更新完缓存之后能对缓存进行删除。

但是睡眠多久就是一种玄学了,比较难评估出来,所以这种方案也只是尽可能保证最终一致性,极端条件下任然有缓存不一致的情况

对于更新数据库+删除缓存的策略来说,如果删除缓存失败了,那么就会出现数据不一致,解决办法有两种

  1. 重试机制

    对于先更新数据库再删除缓存的策略来说,引入消息队列。将待删除的缓存数据加入消息队列中,如果缓存删除失败了,消费者再去消息队列重新读取数据,继续删除缓存(异步)。如果超过一定次数,就报告给客户端。如果删除成功了,就把消息队列里的缓存数据删除,避免重复操作

  2. 订阅MySQL binlog,再操作缓存

    通过MySQL更新数据库成功后,会产生一条变更日志,会存储在binlog中。这样,我们可以订阅binlog,解析得到数据,然后再执行删除缓存数据。阿里巴巴的canal中间件是基于这个实现的。在这个基础上我们还可以加上消息队列进行保存数据,进行重试删除。

    canal模拟MySQL主从复制的交互协议,将自己伪装为slave,向主节点发送dump请求,MySQL收到请求后,会发送binlog给canal,canal解析binlog字节流之后,转换成方便读取的结构化数据,供下游程序订阅使用。

    当订阅 MySQL binlog后,检测到有数据更新了(比如A用户的记录更新),那么redis就把这个数据(A用户)对应的缓存删掉。

这两种操作都有一条共同点,都是采用异步操作缓存的。

缓存雪崩

对于缓存,为了保证数据一致性,我们通常设置过期时间,当数据过期了就会去数据库读取,然后重新更新缓存。

当有大量的数据在同一时间过期或者Redis故障宕机了,此时有大量请求这些数据,都无法在redis中处理,就会全部去数据库进行查询,此时数据库压力倍增,严重的话会导致数据库宕机,进而会导致整个系统崩溃,这就是缓存雪崩。

针对大量数据过期

  1. 均匀设置过期时间,将过期时间加上一个随机值,这样就保证不会有大量数据在同一时间过期

  2. 互斥锁,保证每次只有一个请求更新缓存,其他线程要么阻塞等待互斥锁解锁后读取数据,要么返回空值或默认值。实现互斥锁时最好设置超时时间,避免某种意外没有释放锁,其他请求也获取不到,就会造成系统无响应。

  3. 双key操作,设置两个key,主key设置过期时间,副key不设置过期时间,两个key是不一样的,但是值一样。当业务访问不到主key缓存数据时,直接返回副key缓存数据,在更新缓存时同时更新主key和副key的数据。好处是可以快速响应,不用因为key失效而导致大量请求被锁阻塞主(采用互斥锁,仅一个请求构建缓存),后序再通知后台线程,重新构建主key的数据

  4. 后台更新缓存

    业务线程不再更新缓存,缓存数据也不设置有效期,而是通过后台线程定时更新缓存。当系统内存紧张时,缓存数据被淘汰出去之后,业务线程访问线程未命中,则会返回空,业务视角来看就是数据丢失了。

    解决办法:

    1. 后台线程不仅定时更新缓存,并且频繁的查询缓存是否有效。当因为系统内存紧张而缓存数据被淘汰之后,后台线程查询不到缓存,则立刻从数据库查询数据然后更新缓存。这种办法定时间隔不能太长,不然业务也可能查询不到缓存而返回空或者是一个错误的值。
    2. 设置消息队列。当业务查询不到缓存时,则通过消息队列通知后台进行缓存更新。后台线程收到消息之后,先去查询缓存是否存在,如果存在则不执行更新缓存的操作,如果不存在,则去数据库查询数据并且更新缓存。

针对Redis故障宕机

  1. 服务熔断或者请求限流

    1. 因为Redis故障而导致缓存雪崩的问题时,我们可以启用服务熔断的机制,暂停业务对缓存数据的访问,直接返回错误,等到Redis正常之后,再启用业务访问缓存服务。但是这样整个业务都没法正常工作。
    2. 为了减少对业务的影响,我们可以启用请求限流的机制。只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到Redis恢复正常之后并且把缓存预热之后,再解除请求限流的机制。
  2. 构建Redis缓存高可用集群

    最好是通过主从复制的方式构建Redis缓存高可靠集群。如果Redis主节点宕机,那么迅速切换从节点继续提供服务。

缓存击穿

**当某个热点数据过期之后,此时业务大量访问该热点数据,就无法从缓存中获取数据,直接访问数据库,数据库就很容易被高并发的请求冲垮,这就是缓存击穿。**缓存击穿其实可以看成缓存雪崩的子集。

解决办法:

  1. 互斥锁方案,保证同一时间只有一个业务请求更新缓存。未能获取到锁的请求,要么等待锁的释放重新获取缓存数据,要么返回空值或默认值。
  2. 不设置过期时间,由后台线程异步定时更新缓存数据并且检测缓存是否失效,或者在热点数据即将过期之前,提前通知后台线程进行更新缓存并且设置过期时间

缓存穿透

访问的数据不存在缓存中并且不存在数据库中,这时业务访问缓存,发现没有命中,再去访问数据库,发现数据库不存在要访问的数据,**这时就没办法构建缓存,来服务后序的请求。**当有大量的请求到来时,数据库压力倍增,这就是缓存穿透。

解决办法:

  1. **非法请求的限制。**当有大量的非法请求访问不存在的数据时,会造成缓存穿透。这时我们可以在API入口处判断访问的数据参数是否含有非法值,参数是否合理,字段是否存在,如果判断为恶意请求,直接返回错误,避免进一步访问数据库

  2. 缓存空值或者默认值。

    当我们发现线上业务有缓存穿透的现象,可以针对查询的数据,在缓存中设置一个空值或默认值。这样后序请求就可以从缓存中得到空值或默认值,而不用直接访问数据库

  3. 布隆过滤器快速判断缓存数据是否存在,避免通过查询数据库来判断数据是否存在

    在写入数据库的时候,在布隆过滤器中做一个标记,然后在业务请求到来的时候,业务查询到缓存失效后,再去布隆过滤器中快速判断数据是否存在,如果不存在就不用通过数据库来判断数据是否存在。

    即便发生了缓存穿透,大量业务也只会访问Redis和布隆过滤器,不会访问数据库

    布隆过滤器

    布隆过滤器由N个哈希函数和初始值都为0的位图数组组成。加入数据时,先对数据的进行N个哈希函数的运算,得到N个哈希值,然后对这N个哈希值进行取模,再将位图数组上对应的坐标置为1,当查询数据时,同样通过N个哈希函数得到N个哈希值,然后取模得到对应的下标,只有这n个下标的值全为1,才代表数据存在,否则就不存在

    由于不同的值进行哈希运算时可能存在哈希冲突,所以查询布隆过滤器说数据存在,并不能代表数据库存在这个数据,但是查询不到,一定能够代表数据库不存在这个数据。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值