Redis干货
主流应用架构
客户端->缓存->数据库
数据库挂掉,缓存提供熔断机制,直接返回数据,不访问数据库
memcache与redis区别
memcache 代码层面类似哈希 支持简单数据类型 不支持持久化 不支持主从 不支持分片(sharing,将大数据分配到多个物理节点存储的方案)
redis 数据类型丰富 支持磁盘存储,支持主从,支持分片
redis为什么快
10W qps (每秒查询次数)
完全基于内存,不会受到硬盘IO性能限制
数据结构简单,对数据操作简单,非关系型数据库,键值对取值快
单进程单线程,也能处理高并发请求,对于客户端的请求通过一个线程处理,主线程串行处理,不会有并发安全问题。采用多路复用模型,非阻塞IO,周期性处理多个请求。
redis支持qps非常高,团队已验证CPU不是制约redis性能的瓶颈,想利用多核处理器可以构建多个redis实例
redis并不是只有一个线程,比如在持久化时会单独新建一个进程来执行。
多路I/O复用模型
文件描述符(FD file descriptor)一个打开的文件通过唯一的描述符进行引用。打开文件的元数据到文件本身的映射
传统阻塞模型,文件有读写请求时,文件描述符(FD file descriptor)不可读或不可写时,会阻塞。
thread->select->(channel,channel...)
select同时监控多个文件描述符的可读可写,返回多个文件描述符来执行。
redis采用的I/O的多路复用函数与平台有关,比如epoll函数在linux平台,时间复杂度为O(1).select函数是保底用的,效率较低,
时间复杂度为O (n).
基于回调(react)模式,监听I/O事件,当有读写请求时,回调FD绑定的文件事件处理器,通过多路复用模型实现对多个FD的监控。
数据类型
String 最基本的数据类型,二进制安全,可包含任何数据,比如存储数字,字符串,图片,序列化文件等,
当字符串长度小于1MB时,扩容空间采用加倍策略。
当字符串长度大于1MB时,每次扩容只会多分配1MB大小的冗余空间。
另外Redis字符串的长度不得超过512MB.
简单动态字符串
底层伪代码为{
int a; //已被使用的空间长度
int b;//未被使用的空间长度
char[] c;//保存数据
}
指令
set name "redis"
OK
get name
"redis"
set name "memcache"
OK
get name
"memcache"
set count 1
OK
get count
"1"
incr count
OK
get count
"2"
hash String元素组成的字典,用于存储对象
hmset lilei name "lilii" age 21 title "senior"
OK
hget lilei age
"21"
hset lilii title "pr"
OK
hget lilei title
"pr"
List 列表,按照String元素插入顺序排序。后进先出。通过双端链表实现。应用:最新消息排行榜
lpush mylist aaa
1
lpush mylist bbb
2
lpush mylist ccc
3
lrange my list 0 10
1) "ccc"
2) "bbb"
3) "ccc"
Set String元素组成的无序集合,通过哈希表实现,不允许重复.可以求交集,并集 应用:共同关注
sadd myset 111
1
sadd myset 222
1
sadd myset 333
1
sadd myset 222
0
smembers myset
1) "111"
2) "333"
3) "222"
Sorted Set(Zset) 有序集合 每个元素会关联一个double类型的分数,通过分数为集合中的成员进行从小到大的排序;
通过跳表实现
应用给队列加权重,普通消息分数为1,重要消息为2,线程根据分数高低优先获取重要消息
zadd myzset 3 abc
1
zadd myzset 1 add
1
zadd myzset 2 abb
1
zadd myzset 2 abb
0
zadd myzset 1 big
1
zrangebyscore myzset 0 10
1) "add"
2) "big"
3) "abb"
4) "abc"
此外,还有用于计数的HyperLogLog,用于支持存储地理位置信息的Geo
这些数据类型依据的是底层数据类型基础:
- 简单动态字符串
- 链表。双向链表
- 字典。hash表 链地址法解决哈希冲突
- 跳表。
- 整数集合
- 压缩列表
- 对象
大多数情况下,Redis使用简单字符串SDS作为字符串的表示,相对于C语言字符串,SDS具有常数复杂度获取字符串长度,杜绝了缓存区的溢出,减少了修改字符串长度时所需的内存重分配次数,以及二进制安全能存储各种类型的文件,并且还兼容部分C函数。
通过为链表设置不同类型的特定函数,Redis链表可以保存各种不同类型的值,除了用作列表键,还在发布与订阅、慢查询、监视器等方面发挥作用(后面会介绍)。
Redis的字典底层使用哈希表实现,每个字典通常有两个哈希表,一个平时使用,另一个用于rehash时使用,使用链地址法解决哈希冲突。
跳跃表通常是有序集合的底层实现之一,表中的节点按照分值大小进行排序。
整数集合是集合键的底层实现之一,底层由数组构成,升级特性能尽可能的节省内存。
压缩列表是Redis为节省内存而开发的顺序型数据结构,通常作为列表键和哈希键的底层实现之一。
从海量key中查询某一固定前缀的key
keys指令可以一次性获取到所有某一固定前缀的key
//返回前缀为k1的所有key
keys k1*
缺点:由于一次返回所有key,那么数据量大的时候,会消耗大量内存,执行时间长影响性能,造成服务卡顿。
解决办法
scan指令 基于游标的迭代器,每次返回从游标开始的一部分key,增量式返回,分批次执行后会返回带有重复数据的结果,需要在客户端代码里new一个hashset,实现去重。
//开始迭代,从0开始,返回10个以内的以k1为前缀的key
scan 0 match k1* count 10
//执行后会返回游标和部分key
1)"2221"
2) 1)"k13333"
2)"k13324"
//继续 移动游标
scan 2221 match k1 count 10
java中引入Jedis 包,可以在代码中调用上述指令
Redis实现分布式锁
分布式锁需要解决的问题(互斥性,安全性,死锁,容错)
SETNX key value :如果kay不存在,则创建并赋值,返回1;否则操作失败,返回0;时间复杂度O(1)
get lock
nil
setnx lock test
1
get lock
test
setnx lock task
0
get lock
test
//解决setnx长期有效的问题 设置过期时间5s
expire lock
1
//程序中实现分布式锁的伪代码
RedisService redis = SpringUtils.getBean(RedisService.class);
long status = redis.setnx(key,"1");
if(status==1){
redis.expire(key, time);
//执行同步代码
work();
....;
}
这种实现的问题
setnx是原子操作,expire也是原子操作;但是两个组合在一起就不是,比如说,setnx设置成功后,redis挂了,此时expire就会设置失败,然后这个key就会一直存在,造成死锁。
解决办法
//这个指令相当于同时实现了setnx和expire,设置了过期时间10s
set lock 12345 ex 10 nx
大量key同时过期
解决:在给key设置过期时间时,加上随机值。
如何使用redis做异步队列
使用list作为队列,rpush生成消息 lpop消费消息
rpush testlist aaa
1
rpush testlist bbb
2
rpush testlist ccc
3
lpop testlist
"aaa"
lpop testlist
"bbb"
lpop testlist
"ccc"
缺点:没有等待队列里有值就直接消费
弥补:在应用层引入sleep机制去调用lpop重试
其他方法 blpop
lpop testlist
nil
//等待30s直到有值再pop
blpop testlist 30
发布订阅模式
多个redis客户端 ,举个例子
流程如下:
两个客户端a和b都执行 subcribe myTopic 订阅
客户端c执行 subcribe anotherTopic
客户端t 发布消息 执行 publish myTopic "hello" 发布
此时 a和b会 收到消息 "hello"
客户端t 发布消息 执行 publish myTopic "i love u" 发布
此时 a和b会 收到消息 "i love u"
客户端t 发布消息 执行 publish anotherTopic "fuck u" 发布
此时 c会 收到消息 "fuck u"
客户端只会收到它订阅的消息
缺点:消息的发布是无状态的,即发布后无法确定是否被接收到,是否在传输中丢失,消息即发即失,
比如某个消费者在接收某条消息时下线,再上线后是收不到这条消息的,所以我们最好使用专业的mq来实现发布订阅。
Redis持久化方案
RDB持久化:保存某个时间点的全量数据的快照
//持久化的时间策略,配置多种规则,redis每个时间段读写请求是平衡,所以自定义规则
//900秒内如果有一条写入,则备份一次
save 900 1
//300秒内有10条写入,则备份一次
save 300 10
//60秒内有10000条写入,则备份一次
save 60 10000
//备份进程出错,主进程停止写入
stop-writes-on-bgsave-error yes
//备份时对rdb文件进行压缩
rdbcompression yes
save:阻塞Redis的服务器进程,直到RDB文件被创建完毕(很少使用,会阻塞主线程工作)
bgsave:fork出一个子进程来创建rdb文件,不阻塞服务器进程(fork子进程,copy-on-write,当修改资源时,会复制一份副本,其他调用者所见到的最初的资源保持不变)
自动化触发RDB持久化的方式
根据redis.conf配置里的save m n 定时触发(用的是BGSAVE)
主从复制时,主节点自动触发
执行Debug Reload
执行Shutdown且没有开启AOF持久化
缺点:
内存数据的全量同步,数据量大会由于I/O而严重影响性能
可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据
AOF持久化:保存写状态
记录下出了查询以外的所有变更数据库状态的指令
以append的形式追加保存到AOF文件中(增量)
//以aof方式备份
appendonly yes
//aof文件名
appendfilename appendonly.aof
//每秒追加写入
appendfsync everysec
aof文件支持重写,以减小文件大小(比如有100条一样的写指令,重写会合并成一条)
aof持久化过程如下:
- 调用fork(),创建一个子进程
- 子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件
- 主进程持续将新的变动同时写到内存和原来的AOF里
- 主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动
- 使用新的AOF文件替换掉旧的AOF文件
redis文件恢复流程
rdb和aof文件共存情况下的恢复流程
如果aof存在,会忽略掉rdb文件,直接加载aof文件,否则通过rdb恢复
rdb和aof的优缺点
rbd优点:全量数据快照,文件小,恢复快
rdb缺点:无法保存最近一次快照之后的数据
aof优点:可读性高,适合保存增量数据,数据不易丢失(丢失的往往是设置的1s的数据)
aof缺点:文件大(可重写),恢复时间长
RDB-AOF混合持久化方式(redis默认)
bgsave做镜像全量持久化,aof做增量持久化。
使用pipeline的好处
redis时请求-响应模型,意味着下一个请求会等上一个请求响应后结束才会发送。pipeline可支持一次性发送多个请求,批量执行指令,减少IO次数,需注意:请求之间需满足没有顺序依赖
Redis主从同步
原理: 1个master 负责写 其他salve 读。备份一般由其中一个salve来执行,多个节点之间不需要保证强一致性,只要满足最终一致性即可,最大程度保证redis的性能
全量同步过程:
salve发送sync命令到Master
Master启动一个后台进程,将redis中的数据快照保存到文件中
Master将保存数据快照期间接收的写命令缓存起来
Master完成写文件操作后,将该文件发送给salve
salve将新的AOF文件替换掉旧的AOF文件,读文件进行同步
Master将这期间收集的增量写指令发送给salve端,进行同步
增量同步:
Master接收到用户的操作指令,判断是否需要传播到Slave
将操作记录追加到AOF文件
将操作传播到其他Slave:1.对齐主从库,2,往响应缓存写入指令
将缓存中的数据发送给slave
Redis哨兵
解决主从同步Master宕机后的主从切换问题
监控:检查主从服务器是否运行正常
提醒:通过API向管理员或者其他应用程序发送故障通知
自动故障迁移:主从切换,将其中一个slave变为master
Redis集群
如何从海量数据里快速找到所需
分片:建立多个redis服务器节点,将数据按照规则分散存储到多个服务器节点,实现数据分片,降低单节点压力
常规的按照key的哈希值,根据节点数取模,但是缺点是,在我们增加或减少节点时,这种算法会导致大量的key无法命中,
为了解决这种问题,我们采用一致性哈希的算法
一致性哈希
对2的32次方取模,将整个哈希值空间组织称虚拟的圆环(0,2的32方-1),按顺时针将服务器节点和数据均使用相同的哈希算法映射在圆环上。所有的数据都会按照顺时针的方向存储在最近的服务器节点。
这样如果一台服务器不可用,只会影响到这台服务器和上台服务器之间的数据,受影响的数据量会很小。
缺点:服务器节点较少时,会导致在哈希环上分配不均匀,数据倾斜导致数据存储不平衡。
解决办法:设计虚拟节点,将实际节点计算多个哈希,分散成多个虚拟节点(node1变为node1#1,node1#2,node1#3等)
Redis的应用场景
一:缓存——热数据
热点数据(经常会被查询,但是不经常被修改或者删除的数据),首选是使用redis缓存,毕竟强大到冒泡的QPS和极强的稳定性不是所有类似工具都有的,而且相比于memcached还提供了丰富的数据类型可以使用,另外,内存中的数据也提供了AOF和RDB等持久化机制可以选择,要冷、热的还是忽冷忽热的都可选。
结合具体应用需要注意一下:很多人用spring的AOP来构建redis缓存的自动生产和清除,过程可能如下:
Select 数据库前查询redis,有的话使用redis数据,放弃select 数据库,没有的话,select 数据库,然后将数据插入redis
update或者delete数据库前,查询redis是否存在该数据,存在的话先删除redis中数据,然后再update或者delete数据库中的数据
上面这种操作,如果并发量很小的情况下基本没问题,但是高并发的情况请注意下面场景:
为了update先删掉了redis中的该数据,这时候另一个线程执行查询,发现redis中没有,瞬间执行了查询SQL,并且插入到redis中一条数据,回到刚才那个update语句,这个悲催的线程压根不知道刚才那个该死的select线程犯了一个弥天大错!于是这个redis中的错误数据就永远的存在了下去,直到下一个update或者delete。
二:计数器
诸如统计点击数等应用。由于单线程,可以避免并发问题,保证不会出错,而且100%毫秒级性能!爽。
命令:INCRBY
当然爽完了,别忘记持久化,毕竟是redis只是存了内存!
三:队列
·
相当于消息系统,ActiveMQ,RocketMQ等工具类似,但是个人觉得简单用一下还行,如果对于数据一致性要求高的话还是用RocketMQ等专业系统。
由于redis把数据添加到队列是返回添加元素在队列的第几位,所以可以做判断用户是第几个访问这种业务
队列不仅可以把并发请求变成串行,并且还可以做队列或者栈使用
四:位操作(大数据处理)
用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。
想想一下腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,你能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多,腾讯光这个得多花多少钱。。)好吧。这里要用到位操作——使用setbit、getbit、bitcount命令。
原理是:
redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示我们上面例子里面的用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统,上面我说的几个场景也就能够实现。用到的命令是:setbit、getbit、bitcount
五:分布式锁与单线程机制
·
验证前端的重复请求(可以自由扩展类似情况),可以通过redis进行过滤:每次请求将request Ip、参数、接口等hash作为key存储redis(幂等性请求),设置多长时间有效期,然后下次请求过来的时候先在redis中检索有没有这个key,进而验证是不是一定时间内过来的重复提交
秒杀系统,基于redis是单线程特征,防止出现数据库“爆破”
全局增量ID生成,类似“秒杀”
·
六:最新列表
例如新闻列表页面最新的新闻列表,如果总数量很大的情况下,尽量不要使用select a from A limit 10这种low货,尝试redis的 LPUSH命令构建List,一个个顺序都塞进去就可以啦。不过万一内存清掉了咋办?也简单,查询不到存储key的话,用mysql查询并且初始化一个List到redis中就好了。
七:排行榜
谁得分高谁排名往上。命令:ZADD(有续集,sorted set)
这个需求与上面需求的不同之处在于,取最新N个数据的操作以时间为权重,这个是以某个条件为权重,比如按顶的次数排序,这时候就需要我们的sorted set出马了,将你要排序的值设置成sorted set的score,将具体的数据设置成相应的value,每次只需要执行一条ZADD命令即可。
八:交集并集
在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
Redis与Memcached的区别与比较
1 、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
2 、Redis支持数据的备份,即master-slave模式的数据备份。
3 、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中
4、 redis的速度比memcached快很多
5、Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的IO复用模型。
Redis与Memcached的选择
终极策略: 使用Redis的String类型做的事,都可以用Memcached替换,以此换取更好的性能提升; 除此以外,优先考虑Redis;
使用redis有哪些好处?
(1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
(2)支持丰富数据类型,支持string,list,set,sorted set,hash
(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
(4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
redis如何利用多核的性能?
redis的读取和处理性能非常强大,一般服务器的cpu都不会是性能瓶颈。redis的性能瓶颈主要集中在内存和网络方面。所以,如果使用的redis命令多为O(N)、O(log(N))时间复杂度,那么基本上不会出现cpu瓶颈的情况。
但是如果你确实需要充分使用多核cpu的能力,那么需要在单台服务器上运行多个redis实例(主从部署/集群化部署),并将每个redis实例和cpu内核进行绑定(使用 taskset命令。
如果需要进行集群化部署,你需要对redis进行分片存储
场景:有海量key和value都比较小的数据,在redis中如何存储才更省内存。
原理:通过大幅减少key的数量来降低内存的消耗。
实现:hash hset,在客户端通过分组将海量的key根据一定的策略映射到一组hash对象中,由于value较小,故hash类型的对象会使用占用内存较小的ziplist编码。
eg:如存在100万个键,可以映射到1000个hash中,每个hash保存1000个元素。
缓存和数据库一致性解决方案
首先需要考虑到:更新数据库或者更新缓存都有可能失败,在这种前提下分析业务带来的影响。
一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。
串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
Cache Aside Pattern
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。
比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都把里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。
最初级的缓存不一致问题及解决方案
问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。
比较复杂的数据不一致问题分析
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了...
为什么上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。
解决方案如下:
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据+更新缓存”的操作,根据唯一标识路由之后,也发送到同一个 jvm 内部队列中。
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
1.第一种方案:采用延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
伪代码如下
public void write( String key, Object data )
{
redis.delKey( key );
db.updateData( data );
Thread.sleep( 500 );
redis.delKey( key );
}
2.具体的步骤就是:
- 先删除缓存
- 再写数据库
- 休眠500毫秒
- 再次删除缓存
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。
3.设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
4.该方案的弊端
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
2、第二种方案:异步更新缓存(基于订阅binlog的同步机制)
1.技术整体思路:
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
- 读Redis:热数据基本都在Redis
- 写MySQL:增删改都是操作MySQL
- 更新Redis数据:MySQ的数据操作binlog,来更新到Redis
2.Redis更新
(1)数据操作主要分为两大块:
- 一个是全量(将全部数据一次写入到redis)
- 一个是增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delate变更数据。
(2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis!
缓存雪崩
缓存雪崩是由于原有缓存失效(过期),新缓存未到期间。所有请求都去查询数据库,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
1. 碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
2. 加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法。
还有一个解决办法解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存。
缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
这样做后,就可以一定程度上提高系统吞吐量。
缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决的办法就是:如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样避免,用户请求的时候,再去加载相关的数据。
解决思路:
1,直接写个缓存刷新页面,上线时手工操作下。
2,数据量不大,可以在WEB系统启动的时候加载。
3,定时刷新缓存,
Redis的过期键的删除策略
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
Redis key的过期时间和永久有效分别怎么设置?
EXPIRE和PERSIST命令。
我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
定时去清理过期的缓存;
当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
Redis事务的概念
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis事务的三个阶段
事务开始 MULTI
命令入队
事务执行 EXEC
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队
Redis事务相关命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
如果在一个事务中的命令出现语法错误,那么所有的命令都不会执行;
如果在一个事务中出现运行错误,那么正确的命令会被执行。
WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
UNWATCH命令可以取消watch对所有key的监控
选举机制
Redis选举领头Sentinel
Sentinel是Redis实现高可用的保证。Sentinel系统作用就是监视Redis服务器集群,它可以不停的获得redis集群状态,当一个主节点挂了,故障转移操作会在从节点中选出一个新的主节点,这里故障转移就是由Sentinel来主导完成的。
不要把Sentinel想的太复杂,它其实就是一个特殊工作模式的Redis服务器而已,Redis是集群部署的,这里的Sentinel也是要集群部署的,要是非单点部署,你的Sentinel挂了,此时的Redis集群就GG了。
接着上边说,当主服务器节点挂了,Sentinel系统就会选出一个领头的Sentinel来完成故障转移工作。选举规则如下: - 监视这个挂了的主节点的所有Sentinel都有被选举为领头的资格
- 每进行一次选举,不论是否成功,配置纪元+1,配置纪元就是个计数器
- 每个Sentinel在每个配置纪元中有且仅有一次选举机会,一旦选好了该节点认为的主节点,在这个纪元内,不可以再更改
- 每个发现服务器挂了的Sentinel都会配置纪元+1并投自己一票,接着发消息要求其他Sentinel设置自己为领头人1,每个Sentinel都想成为领头的
- 每个Sentinel会将最先发来请求领头的节点设为自己的领头节点并发送回复,谁先来我选谁
- 当源Sentinel收到回复,并且回复中的配置纪元和自己的一致且领头Id是自己的Sentinel Id时,表明目标Sentinel已经将自己设为领头
- 在一个配置纪元内,当某个Sentinel收到半数以上的同意回复时,它就是领头的了
- 如果在给定时间内,没有被成功选举的Sentinel,那么过段时间发起新的选举
选举领头Sentinel的过程和规则大概就如上所述,需要注意的是只有集群出现节点挂了才需要选举出领头Sentinel,平时每个Sentinel还是平等身份~
这里的选举其实是raft算法的一个应用,有兴趣的小伙伴可以去读下这篇算法的论文
Zookeeper选举
Zookeeper是一个很强的分布式数据一致性解决方案,比如dubbo中的注册中心就使用的Zookeeper。当然,这也是集群部署的,但是它没有采用传统的Master/Slave结构,而是引入了Leader、Follwer和Observer。Leader和Follower类似于Master/Slave,新增的Observer作用仅仅只是增加集群的读性能,它不参与Leader的选举。
节点的状态有以下几种:
- LOOKING: 节点正处于选主状态,不对外提供服务,直至选主结束;
- FOLLOWING: 作为系统的从节点,接受主节点的更新并写入本地日志;
- LEADING: 作为系统主节点,接受客户端更新,写入本地日志并复制到从节点
Zookeeper的状态同步是基于Zab协议实现的,Zab协议有两种模式,它们分别是崩溃恢复(选主)和消息广播(同步)。当服务启动或者在Leader崩溃后,Zab就进入了恢复模式,当Leader被选举出来,且超过一半机器完成了和 leader的状态同步以后,恢复模式就结束了。
我们来重点看下选主是怎么完成的
首先明确几个概念: - Sid:服务器id;
- Zxid:服务器的事务id,数据越新,zxid越大;zxid的高32位是epoch,低32位是zpoch内的自增id,由0开始。每次选出新的Leader,epoch会递增,同时zxid的低32位清0。
整个选主流程如下
- 状态变更。服务器启动的时候每个server的状态时Looking,如果是leader挂掉后进入选举,那么余下的非Observer的Server就会将自己的服务器状态变更为Looking,然后开始进入Leader的选举状态;
- 发起投票。每个server会产生一个(sid,zxid)的投票,系统初始化的时候zxid都是0,如果是运行期间,每个server的zxid可能都不同,这取决于最后一次更新的数据。将投票发送给集群中的所有机器;
- 接收并检查投票。server收到投票后,会先检查是否是本轮投票,是否来自looking状态的server;
- 处理投票。对自己的投票和接收到的投票进行PK:
先检查zxid,较大的优先为leader;如果zxid一样,sid较大的为leader;根据PK结果更新自己的投票,再次发送自己的投票;
- 统计投票。每次投票后,服务器统计投票信息,如果有过半机器接收到相同的投票,那么leader产生,如果否,那么进行下一轮投票;
- 改变server状态。一旦确定了Leader,server会更新自己的状态为Following或者是Leading。选举结束。
我们要保证选主完成后,原来的主节点已经提交的事务继续完成提交;原主节点只是提出而没提交的事务要抛弃。这也是为什么倾向于选zxid最大的从节点为主节点,因为它上边的事务最新,最与原主节点保持一致。
总结
- Redis中的Sentinel(哨兵)选主相对来说更简单,因为不涉及事务状态的一致性
- Sentinel选主是基于raft协议,Zookeeper则基于Zab协议
- 二者都是收到半数的选票就选举成功
- Sentinel投票发消息主要内容是Sentinel id和配置纪元,Zookeeper则是 zxid和 sid
- Sentinel谁先来找他投票他就投谁,Zookeeper中则是要细细检查比较一番,检查内容包括epoch和节点状态,检查完毕后再跟自己的投票进行pk,进而看需不需要更新自己的投票,若是需要,则自己的投票也要广播出去
Redis的列表数据结构可以让我们方便的实现消息队列
例如用LPUSH(BLPUSH)把消息入队,用 RPOP(BRPOP)获取消息
绝大部分的情况下,这些操作都是没问题的,但并不能保证绝对安全
当LPOP 返回一个元素给客户端的时候,会从 list 中把该元素移除,这意味着该元素就只存在于客户端的上下文中,如果客户端在处理这个返回元素的过程崩溃了,那么这个元素就永远丢失了
如何解决?
redis 有一个 RPOPLPUSH (或者其阻塞版本的 BRPOPLPUSH)命令
命令格式
RPOPLPUSH source destination
原子性地返回并移除source 列表的最后一个元素, 并把该元素放入 destination 列表的头部
用这个命令可以保证队列的安全问题:
使用RPOPLPUSH 获取消息时,RPOPLPUSH 会把消息返给客户端,同时把该消息放入一个备份消息列表,并且这个过程是原子的,可以保证消息的安全,当客户端成功的处理了消息后,就可以把此消息从备份列表中移除了