Redis基础
Redis篇
什么是Redis
Redis是一款内存高速缓存数据库。Redis全称为:Remote Dictionary Server(远程数据服务),使用C语言编写,Redis是一个key-value存储系统(键值存储系统),支持丰富的数据类型,如:String、list、set、zset、hash。
Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
WhyRedis
一个产品的使用场景肯定是需要根据产品的特性,先列举一下Redis的特点:
- 读写性能优异
- Redis能读的速度是110000次/s,写的速度是81000次/s (测试条件见下一节)。
- 数据类型丰富
- Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子性
- Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
- 丰富的特性
- Redis支持 publish/subscribe, 通知, key 过期等特性。
- 持久化
- Redis支持RDB, AOF等持久化方式
- 发布订阅
- Redis支持发布/订阅模式
- 分布式
- Redis Cluste
总结:速度快,完全基于内存,使用C语言实现,网络层使用epoll解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件;
其他类型产品:
GuavaCache | Tair | EVCache | Aerospike | |
---|---|---|---|---|
类别 | 本地JVM缓存 | 分布式缓存 | 分布式缓存 | 分布式nosql数据库 |
应用 | 本地缓存 | 淘宝 | Netflix、AWS | 广告 |
性能 | 非常高 | 较高 | 很高 | 较高 |
持久化 | 无 | 有 | 有 | 有 |
集群 | 无 | 灵活配置 | 有 | 自动扩容 |
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
不同产品的介绍:
1、redis
1)完全基于内存,绝大部分请求是纯粹的内存操作。数据存在内存中,类似于 HashMap,查找和操作的时间复杂度都是O(1);
2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3)采用单线程,避免了多线程不必要的上下文切换和竞争条件,不存在加锁释放锁操作,减少了因为锁竞争导致的性能消耗;(6.0以后多线程)
4)使用EPOLL多路 I/O 复用模型,非阻塞 IO;
5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis的使用场景
redis应用场景总结redis平时我们用到的地方蛮多的,下面就了解的应用场景做个总结:
# 热点数据的缓存
缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。
作为缓存使用时,一般有两种方式保存数据:
- 读取前,先去读Redis,如果没有数据,读取数据库,将数据拉入Redis。
- 插入数据时,同时写入Redis。
方案一:实施起来简单,但是有两个需要注意的地方:
- 避免缓存击穿。(数据库没有就需要命中的数据,导致Redis一直没有数据,而一直命中数据库。)
- 数据的实时性相对会差一点。
方案二:数据实时性强,但是开发时不便于统一处理。
当然,两种方式根据实际情况来适用。如:方案一适用于对于数据实时性要求不是特别高的场景。方案二适用于字典表、数据量不大的数据存储。
# 限时业务的运用
redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
# 计数器相关问题
redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
# 分布式锁
这个主要利用redis的setnx命令进行,setnx:”set if not exists”就是如果不存在则成功设置缓存同时返回1,否则返回0 ,这个特性在很多后台中都有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock, 如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。 当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。
在分布式锁的场景中,主要用在比如秒杀系统等。
# 延时操作
比如在订单生产后我们占用了库存,10分钟后去检验用户是否真正购买,如果没有购买将该单据设置无效,同时还原库存。 由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。 所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。
当然我们也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。
# 排行榜相关问题
关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。
比如点赞排行榜,做一个SortedSet, 然后以用户的openid作为上面的username, 以用户的点赞数作为上面的score, 然后针对每个用户做一个hash, 通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。
# 点赞、好友等相互关系的存储
Redis 利用集合的一些命令,比如求交集、并集、差集等。
在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
# 简单队列
由于Redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。
2、Memcache
redis | Memcached |
---|---|
内存高速数据库 | 高性能分布式内存缓存数据库 |
支持hash、list、set、zset、string结构 | 只支持key-value结构 |
将大部分数据放到内存 | 全部数据放到内存中 |
支持持久化、主从复制备份 | 不支持数据持久化及数据备份 |
数据丢失可通过AOF恢复 | 挂掉后,数据不可恢复 |
单线程(2~4万TPS) | 多线程(20-40万TPS) |
使用场景:
1、如果有持久方面的需求或对数据类型和处理有要求的应该选择redis。
2、如果简单的key/value 存储应该选择memcached。
3、Tair
Tair(Taobao Pair)是淘宝开发的分布式Key-Value存储引擎,既可以做缓存也可以做数据源(三种引擎切换)
- MDB(Memcache)属于内存型产品,支持kv和类hashMap结构,性能最优
- RDB(Redis)支持List.Set.Zset等复杂的数据结构,性能次之,可提供缓存和持久化存储两种模式
- LDB(levelDB)属于持久化产品,支持kv和类hashmap结构,性能较前两者稍低,但持久化可靠性最高
分布式缓存
大访问少量临时数据的存储(kb左右)
用于缓存,降低对后端数据库的访问压力
session场景
高速访问某些数据结构的应用和计算(rdb)
数据源存储
快速读取数据(fdb)
持续大数据量的存入读取(ldb),交易快照
高频度的更新读取(ldb),库存
痛点:redis集群中,想借用缓存资源必须得指明redis服务器地址去要。这就增加了程序的维护复杂度。因为redis服务器很可能是需要频繁变动的。所以人家淘宝就想啊,为什么不能像操作分布式数据库或者hadoop那样。增加一个中央节点,让他去代理所有事情。在tair中程序只要跟tair中心节点交互就OK了。同时tair里还有配置服务器概念。又免去了像操作hadoop那样,还得每台hadoop一套一模一样配置文件。改配置文件得整个集群都跟着改。
4、Guava
分布式缓存一致性更好一点,用于集群环境下多节点使用同一份缓存的情况;有网络IO,吞吐率与缓存的数据大小有较大关系;
本地缓存非常高效,本地缓存会占用堆内存,影响垃圾回收、影响系统性能。
本地缓存设计:
以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况,每个实例都需要各自保存一份缓存,缓存不具有一致性。
解决缓存过期:
1、将缓存过期时间调为永久
2、将缓存失效时间分散开,不要将缓存时间长度都设置成一样;比如我们可以在原有的失效时间基础上增加一个随机值,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
解决内存溢出:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
Google Guava Cache
自己设计本地缓存痛点:
- 不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO 等。
- 清除数据时的回调通知
- 并发处理能力差,针对并发可以使用CurrentHashMap,但缓存的其他功能需要自行实现
- 缓存过期处理,缓存数据加载刷新等都需要手工实现
Guava Cache 的场景:
- 对性能有非常高的要求
- 不经常变化,占用内存不大
- 有访问整个集合的需求
- 数据允许不实时一致
Guava Cache 的优势:
- 缓存过期和淘汰机制
在GuavaCache中可以设置Key的过期时间,包括访问过期和创建过期。GuavaCache在缓存容量达到指定大小时,采用LRU的方式,将不常使用的键值从Cache中删除
- 并发处理能力
GuavaCache类似CurrentHashMap,是线程安全的。提供了设置并发级别的api,使得缓存支持并发的写入和读取,采用分离锁机制,分离锁能够减小锁力度,提升并发能力,分离锁是分拆锁定,把一个集合看分成若干partition, 每个partiton一把锁。更新锁定
- 防止缓存击穿
一般情况下,在缓存中查询某个key,如果不存在,则查源数据,并回填缓存。(Cache Aside Pattern)在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降 GuavaCache可以在CacheLoader的load方法中加以控制,对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。(相当于集成数据源,方便用户使用)
- 监控缓存加载/命中情况
统计
问题:
OOM->设置过期时间、使用弱引用、配置过期策略
5、EVCache
EVCache是一个Netflflix(网飞)公司开源、快速的分布式缓存,是基于Memcached的内存存储实现的,用以构建超大容量、高性能、低延时、跨区域的全球可用的缓存数据层。
E:Ephemeral:数据存储是短暂的,有自身的存活时间
V:Volatile:数据可以在任何时候消失
EVCache典型地适合对强一致性没有必须要求的场合
典型用例:Netflflix向用户推荐用户感兴趣的电影
EVCache集群在峰值每秒可以处理200kb的请求,
Netflflix生产系统中部署的EVCache经常要处理超过每秒3000万个请求,存储数十亿个对象,
跨数千台memcached服务器。整个EVCache集群每天处理近2万亿个请求。
EVCache集群响应平均延时大约是1-5毫秒,最多不会超过20毫秒。
EVCache集群的缓存命中率在99%左右。
典型部署
EVCache 是线性扩展的,可以在一分钟之内完成扩容,在几分钟之内完成负载均衡和缓存预热。
1、集群启动时,EVCache向服务注册中心(Zookeeper、Eureka)注册各个实例
2、在web应用启动时,查询命名服务中的EVCache服务器列表,并建立连接。
3、客户端通过key使用一致性hash算法,将数据分片到集群上。
6、ETCD
和Zookeeper一样,CP模型追求数据一致性,越来越多的系统开始用它保存关键数据。比如,秒杀系统经常用它保存各节点信息,以便控制消费 MQ 的服务数量。还有些业务系统的配置数据,也会通过 etcd 实时同步给业务系统的各节点,比如,秒杀管理后台会使用 etcd 将秒杀活动的配置数据实时同步给秒杀 API 服务各节点。
Redis底层
1、redis数据类型
五种基本数据类型
https://pdai.tech/md/db/nosql-redis/db-redis-data-types.html
类型 | 底层 | 应用场景 | 编码类型 |
---|---|---|---|
String | SDS数组 | 帖子、评论、热点数据、输入缓冲 | RAW << EMBSTR << INT |
List | QuickList | 评论列表、商品列表、发布与订阅、慢查询、监视器 | LINKEDLIST << ZIPLIST |
Set | intSet | 适合交集、并集、查集操作,例如朋友关系 | HT << INSET |
Zset | 跳跃表 | 去重后排序,适合排名场景 | SKIPLIST << ZIPLIST |
Hash | 哈希 | 结构化数据,比如存储对象 | HT << ZIPLIST |
Stream | 紧凑列表 | 消息队列 |
2、相关API
String | SET | SETNX | SETEX | GET | GETSET | INCR | DECR | MSET | MGET |
Hash | HSET | HSETNX | HGET | HDEL | HLEN | HMSET | HMGET | HKEYS | HGETALL |
LIST | LPUSH | LPOP | RPUSH | RPOP | LINDEX | LREM | LRANGE | LLEN | RPOPLPUSH |
ZSET | ZADD | ZREM | ZSCORE | ZCARD | ZRANGE | ZRANK | ZREVRANK | ZREVRANGE | |
SET | SADD | SREM | SISMEMBER | SCARD | SINTER | SUNION | SDIFF | SPOP | SMEMBERS |
事务 | MULTI | EXEC | DISCARD | WATCH | UNWATCH |
3、redis各个类型的底层结构
1. String(字符串)
- 简单动态字符串(SDS)
- 当字符串比较短时,Redis 使用 SDS 结构来存储。SDS 是 Redis 自定义的一种字符串类型,支持高效的字符串操作。
- SDS 结构包含了实际的字符串数据、已用空间长度、总空间长度,以及额外的未使用空间,用于减少内存分配次数和频繁的内存复制。
- 整数编码(int)
- 当字符串表示的是一个整数,并且在特定范围内时,Redis 会使用整数编码来存储该字符串,以节省内存。
2. List(列表)
- 压缩列表(Ziplist):
- 当列表元素数量较少且每个元素的长度都较小时,Redis 使用压缩列表。压缩列表是一种连续内存块,紧凑地存储多个元素,节省内存空间。
- 双向链表(Linked List):
- 当列表元素数量较多或者元素较大时,Redis 使用双向链表。这种结构支持高效的插入和删除操作,但占用的内存较多。
- 快速列表(Quicklist):
- Redis 4.0 引入了一种混合数据结构,结合了压缩列表和双向链表的优点。Quicklist 是由多个小的压缩列表组成的双向链表,既节省内存又保持了操作的高效性。
3. Hash(哈希表)
- 压缩列表(Ziplist):
- 当哈希表中的键值对数量较少,且每个键和值都较短时,使用压缩列表来存储。
- 哈希表(Hashtable):
- 当哈希表中的键值对数量较多或者某些键和值较长时,使用哈希表结构。哈希表使用链地址法解决冲突,提供 O(1) 时间复杂度的查找、插入和删除操作。
4. Set(集合)
- 整数集合(Intset):
- 当集合中的元素都是整数且数量较少时,Redis 使用整数集合存储。整数集合是一种有序且不重复的整数数组。
- 哈希表(Hashtable):
- 当集合中的元素较多或者包含非整数元素时,使用哈希表结构存储。哈希表提供 O(1) 时间复杂度的查找、插入和删除操作。
5. Sorted Set(有序集合)
- 压缩列表(Ziplist):
- 当有序集合中的元素数量较少,并且每个元素的长度较短时,使用压缩列表存储。
- 跳跃表和哈希表(Skip List and Hashtable):
- 当有序集合中的元素较多或者元素较大时,使用跳跃表和哈希表相结合的方式存储。跳跃表用于维持元素的有序性,提供范围查询功能;哈希表用于快速查找元素分值(score),提高操作效率。
Stream
Redis Stream的底层主要使用了listpack(紧凑列表)和Rax树(基数树)。
listpack表示一个字符串列表的序列化,listpack可用于存储字符串或整数。用于存储stream的消息内 容。
Rax树是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操 作。
4、Zset底层实现
跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度
Zset数据量少的时候使用压缩链表ziplist实现,有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。 数据量大的时候使用跳跃列表skiplist和哈希表hash_map结合实现,查找删除插入的时间复杂度都是O(longN)
Redis使用跳表而不使用红黑树,是因为跳表的索引结构序列化和反序列化更加快速,方便持久化。
搜索
跳跃表按 score 从小到大保存所有集合元素,查找时间复杂度为平均 O(logN),最坏 O(N) 。
插入
选用链表作为底层结构支持,为了高效地动态增删。因为跳表底层的单链表是有序的,为了维护这种有序性,在插入前需要遍历链表,找到该插入的位置,单链表遍历查找的时间复杂度是O(n),同理可得,跳表的遍历也是需要遍历索引数,所以是O(logn)。
删除
如果该节点还在索引中,删除时不仅要删除单链表中的节点,还要删除索引中的节点;单链表在知道删除的节点是谁时,时间复杂度为O(1),但针对单链表来说,删除时都需要拿到前驱节点O(logN)才可改变引用关系从而删除目标节点。
Redis可用性
1、redis持久化
持久化就是把内存中的数据持久化到本地磁盘,防止服务器宕机了内存数据丢失
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制,Redis4.0以后采用混合持久化,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备
**RDB:**是Redis DataBase缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
1)只有一个文件 dump.rdb,方便持久化;
2)容灾性好,一个文件可以保存到安全的磁盘。
3)性能最大化,fork 子进程来进行持久化写操作,让主进程继续处理命令,只存在毫秒级不响应请求。
4)相对于数据集大时,比 AOF 的启动效率更高。
缺点:
数据安全性低,RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。
AOF:持久化
AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
优点:
1)数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
2)通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
缺点:
1)AOF 文件比 RDB 文件大,且恢复速度慢。
2)数据集大的时候,比 rdb 启动效率低。
2、redis事务
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis事务的概念
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
事务命令:
**MULTI:**用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
**EXEC:**执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
**WATCH :**是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。(秒杀场景)
**DISCARD:**调用该命令,客户端可以清空事务队列,并放弃执行事务,且客户端会从事务状态中退出。
UNWATCH:命令可以取消watch对所有key的监控。
问题:
redis是单线程的,本身不就是有事务吗,为什么还需要借助这些原语呢?
Redis 确实是单线程的,这意味着每个命令都是原子性的(atomic)。但是,Redis 的事务机制 (MULTI
和 EXEC
) 提供了对一组命令进行原子操作的能力,使得它们在执行时不会被其他命令插入和打断。
3、redis失效策略
内存淘汰策略
1)全局的键空间选择性移除
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(字典库常用)默认·
allkeys-lru:在键空间中,移除最近最少使用的key。(缓存常用)
allkeys-random:在键空间中,随机移除某个key。
2)设置过期时间的键空间选择性移除
volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。
缓存失效策略
**定时清除:**针对每个设置过期时间的key都创建指定定时器
**惰性清除:**访问时判断,对内存不友好
**定时扫描清除:**定时100ms随机20个检查过期的字典,若存在25%以上则继续循环删除。
4、redis读写模式
CacheAside旁路缓存
写请求更新数据库后删除缓存数据。读请求不命中查询数据库,查询完成写入缓存
业务端处理所有数据访问细节,同时利用 Lazy 计算的思想,更新 DB 后,直接删除 cache 并通过 DB 更新,确保数据以 DB 结果为准,则可以大幅降低 cache 和 DB 中数据不一致的概率
如果没有专门的存储服务,同时是对数据一致性要求比较高的业务,或者是缓存数据更新比较复杂的业务,适合使用 Cache Aside 模式。如微博发展初期,不少业务采用这种模式
1 | // 延迟双删,用以保证最终一致性,防止小概率旧数据读请求在第一次删除后更新数据库 |
高并发下保证绝对的一致,先删缓存再更新数据,需要用到内存队列做异步串行化。非高并发场景,先更新数据再删除缓存,延迟双删策略基本满足了
- 先更新db后删除redis:删除redis失败则出现问题
- 先删redis后更新db:删除redis瞬间,旧数据被回填redis
- 先删redis后更新db休眠后删redis:同第二点,休眠后删除redis 可能宕机
- java内部jvm队列:不适用分布式场景且降低并发
Read/Write Though(读写穿透)
先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据.
场景:
微博 Feed 的 Outbox Vector(即用户最新微博列表)就采用这种模式。一些粉丝较少且不活跃的用户发表微博后,Vector 服务会首先查询 Vector Cache,如果 cache 中没有该用户的 Outbox 记录,则不写该用户的 cache 数据,直接更新 DB 后就返回,只有 cache 中存在才会通过 CAS 指令进行更新。
Write Behind Caching(异步缓存写入)
比如对一些计数业务,一条 Feed 被点赞 1万 次,如果更新 1万 次 DB 代价很大,而合并成一次请求直接加 1万,则是一个非常轻量的操作。但这种模型有个显著的缺点,即数据的一致性变差,甚至在一些极端场景下可能会丢失数据。
5、多级缓存
**浏览器本地内存缓存:**专题活动,一旦上线,在活动期间是不会随意变更的。
**浏览器本地磁盘缓存:**Logo缓存,大图片懒加载
**服务端本地内存缓存:**由于没有持久化,重启时必定会被穿透
服务端网络内存缓存:Redis等,针对穿透的情况下可以继续分层,必须保证数据库不被压垮
为什么不是使用服务器本地磁盘做缓存?
当系统处理大量磁盘 IO 操作的时候,由于 CPU 和内存的速度远高于磁盘,可能导致 CPU 耗费太多时间等待磁盘返回处理的结果。对于这部分 CPU 在 IO 上的开销,我们称为 iowait
Redis七大经典问题
1、缓存雪崩
指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃
- 本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 逻辑上永不过期给每一个缓存数据增加相应的缓存标记,缓存标记失效则更新数据缓存
- 多级缓存,失效时通过二级更新一级,由第三方插件更新二级缓存。
2、缓存穿透
https://blog.csdn.net/lin777lin/article/details/105666839
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1)接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2)从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒。这样可以防止攻击用户反复用同一个id暴力攻击;
3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。(宁可错杀一千不可放过一人)
3、缓存击穿
这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
1)设置热点数据永远不过期,异步线程处理。
2)加写回操作加互斥锁,查询失败默认值快速返回。
3)缓存预热
系统上线后,将相关**可预期(例如排行榜)**热点数据直接加载到缓存。
写一个缓存刷新页面,手动操作热点数据**(例如广告推广)**上下线。
4、数据不一致
在缓存机器的带宽被打满,或者机房网络出现波动时,缓存更新失败,新数据没有写入缓存,就会导致缓存和 DB 的数据不一致。缓存 rehash 时,某个缓存机器反复异常,多次上下线,更新请求多次 rehash。这样,一份数据存在多个节点,且每次 rehash 只更新某个节点,导致一些缓存节点产生脏数据。
- Cache 更新失败后,可以进行重试,则将重试失败的 key 写入mq,待缓存访问恢复后,将这些 key 从缓存删除。这些 key 在再次被查询时,重新从 DB 加载,从而保证数据的一致性
- 缓存时间适当调短,让缓存数据及早过期后,然后从 DB 重新加载,确保数据的最终一致性。
- 不采用 rehash 漂移策略,而采用缓存分层策略,尽量避免脏数据产生。
5、数据并发竞争
数据并发竞争在大流量系统也比较常见,比如车票系统,如果某个火车车次缓存信息过期,但仍然有大量用户在查询该车次信息。又比如微博系统中,如果某条微博正好被缓存淘汰,但这条微博仍然有大量的转发、评论、赞。上述情况都会造成并发竞争读取的问题。
- 加写回操作加互斥锁,查询失败默认值快速返回。
- 对缓存数据保持多个备份,减少并发竞争的概率
6、热点key问题
明星结婚、离婚、出轨这种特殊突发事件,比如奥运、春节这些重大活动或节日,还比如秒杀、双12、618 等线上促销活动,都很容易出现 Hot key 的情况。
如何提前发现HotKey?
- 对于重要节假日、线上促销活动这些提前已知的事情,可以提前评估出可能的热 key 来。
- 而对于突发事件,无法提前评估,可以通过 Spark,对应流任务进行实时分析,及时发现新发布的热点 key。而对于之前已发出的事情,逐步发酵成为热 key 的,则可以通过 Hadoop 对批处理任务离线计算,找出最近历史数据中的高频热 key。
解决方案:
- 这 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey,这样就可以把热 key 的请求打散,避免一个缓存节点过载
- 缓存集群可以单节点进行主从复制和垂直扩容
- 利用应用内的前置缓存,但是需注意需要设置上限
- 延迟不敏感,定时刷新,实时感知用主动刷新
- 和缓存穿透一样,限制逃逸流量,单请求进行数据回源并刷新前置
- 无论如何设计,最后都要写一个兜底逻辑,千万级流量说来就来
7、BigKey问题
比如互联网系统中需要保存用户最新 1万 个粉丝的业务,比如一个用户个人信息缓存,包括基本资料、关系图谱计数、发 feed 统计等。微博的 feed 内容缓存也很容易出现,一般用户微博在 140 字以内,但很多用户也会发表 1千 字甚至更长的微博内容,这些长微博也就成了大 key
- 首先Redis底层数据结构里,根据Value的不同,会进行数据结构的重新选择
- 可以扩展新的数据结构,进行序列化构建,然后通过 restore 一次性写入
- 将大 key 分拆为多个 key,设置较长的过期时间
Redis分区容错
1、redis数据分区
Hash:(不稳定)
客户端分片:哈希+取余
节点伸缩:数据节点关系变化,导致数据迁移
迁移数量和添加节点数量有关:建议翻倍扩容
一个简单直观的想法是直接用Hash来计算,以Key做哈希后对节点数取模。可以看出,在key足够分散的情况下,均匀性可以获得,但一旦有节点加入或退出,所有的原有节点都会受到影响,稳定性无从谈起。
一致性Hash:(不均衡)
客户端分片:哈希+顺时针(优化取余)
节点伸缩:只影响邻近节点,但是还是有数据迁移
翻倍伸缩:保证最小迁移数据和负载均衡
一致性Hash可以很好的解决稳定问题,可以将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到先遇到的一组存储节点存放。而当有节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续节点,将数据从该节点接收或者给予。但这又带来均匀性的问题,即使可以将存储节点等距排列,也会在存储节点个数变化时带来数据的不均匀。
Codis的Hash槽
Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进行 crc32 运算计算 哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。
RedisCluster
Redis-cluster把所有的物理节点映射到[0-16383]个slot上,对key采用crc16算法得到hash值后对16384取模,基本上采用平均分配和连续分配的方式。
2、主从模式=简单
主从模式最大的优点是部署简单,最少两个节点便可以构成主从模式,并且可以通过读写分离避免读和写同时不可用。不过,一旦 Master 节点出现故障,主从节点就无法自动切换,直接导致 SLA 下降。所以,主从模式一般适合业务发展初期,并发量低,运维成本低的情况
主从复制原理:
①通过从服务器发送到PSYNC命令给主服务器
②如果是首次连接,触发一次全量复制。此时主节点会启动一个后台线程,生成 RDB 快照文件
③主节点会将这个 RDB 发送给从节点,slave 会先写入本地磁盘,再从本地磁盘加载到内存中
④master会将此过程中的写命令写入缓存,从节点实时同步这些数据
⑤如果网络断开了连接,自动重连后主节点通过命令传播增量复制给从节点部分缺少的数据
缺点
所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决,redis4.0中引入psync2 解决了slave重启后仍然可以增量同步。
3、哨兵模式=读多
由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。哨兵模式适合读请求远多于写请求的业务场景,比如在秒杀系统中用来缓存活动信息。 如果写请求较多,当集群 Slave 节点数量多了后,Master 节点同步数据的压力会非常大。
当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证redis的高可用性。
检测主观下线状态
Sentinel每秒一次向所有与它建立了命令连接的实例(主服务器、从服务器和其他Sentinel)发送PING命 令
实例在down-after-milliseconds毫秒内返回无效回复Sentinel就会认为该实例主观下线(SDown)
检查客观下线状态
当一个Sentinel将一个主服务器判断为主观下线后 ,Sentinel会向监控这个主服务器的所有其他Sentinel发送查询主机状态的命令
如果达到Sentinel配置中的quorum数量的Sentinel实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线(ODown)。
选举Leader Sentinel
当一个主服务器被判定为客观下线后,监视这个主服务器的所有Sentinel会通过选举算法(raft),选出一个Leader Sentinel去执行**failover(故障转移)**操作。
Raft算法
Raft协议是用来解决分布式系统一致性问题的协议。 Raft协议描述的节点共有三种状态:Leader, Follower, Candidate。 Raft协议将时间切分为一个个的Term(任期),可以认为是一种“逻辑时间”。 选举流程: ①Raft采用心跳机制触发Leader选举系统启动后,全部节点初始化为Follower,term为0
②节点如果收到了RequestVote或者AppendEntries,就会保持自己的Follower身份
③节点如果一段时间内没收到AppendEntries消息,在该节点的超时时间内还没发现Leader,Follower就会转换成Candidate,自己开始竞选Leader。 一旦转化为Candidate,该节点立即开始下面几件事情: –增加自己的term,启动一个新的定时器 –给自己投一票,向所有其他节点发送RequestVote,并等待其他节点的回复。
④如果在计时器超时前,节点收到多数节点的同意投票,就转换成Leader。同时通过 AppendEntries,向其他节点发送通知。
⑤每个节点在一个term内只能投一票,采取先到先得的策略,Candidate投自己, Follower会投给第一个收到RequestVote的节点。
⑥Raft协议的定时器采取随机超时时间(选举的关键),先转为Candidate的节点会先发起投票,从而获得多数票。
主服务器的选择
当选举出Leader Sentinel后,Leader Sentinel会根据以下规则去从服务器中选择出新的主服务器。
- 过滤掉主观、客观下线的节点
- 选择配置slave-priority最高的节点,如果有则返回没有就继续选择
- 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整
- 选择run_id最小的节点,因为run_id越小说明重启次数越少
故障转移
当Leader Sentinel完成新的主服务器选择后,Leader Sentinel会对下线的主服务器执行故障转移操作,主要有三个步骤:
1、它会将失效 Master 的其中一个 Slave 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master ;
2、当客户端试图连接失效的 Master 时,集群会向客户端返回新 Master 的地址,使得集群当前状态只有一个Master。
3、Master 和 Slave 服务器切换后, Master 的 redis.conf 、 Slave 的 redis.conf 和 sentinel.conf 的配置文件的内容都会发生相应的改变,即 Master 主服务器的 redis.conf配置文件中会多一行 replicaof 的配置, sentinel.conf 的监控目标会随之调换。
4、集群模式=写多
为了避免单一节点负载过高导致不稳定,集群模式采用一致性哈希算法或者哈希槽的方法将 Key 分布到各个节点上。其中,每个 Master 节点后跟若干个 Slave 节点,用于出现故障时做主备切换,客户端可以连接任意 Master 节点,集群内部会按照不同 key 将请求转发到不同的 Master 节点
集群模式是如何实现高可用的呢?集群内部节点之间会互相定时探测对方是否存活,如果多数节点判断某个节点挂了,则会将其踢出集群,然后从 Slave 节点中选举出一个节点替补挂掉的 Master 节点。整个原理基本和哨兵模式一致
虽然集群模式避免了 Master 单节点的问题,但集群内同步数据时会占用一定的带宽。所以,只有在写操作比较多的情况下人们才使用集群模式,其他大多数情况,使用哨兵模式都能满足需求
5、分布式锁
利用Watch实现Redis乐观锁
乐观锁基于CAS(Compare And Swap)比较并替换思想,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁**(秒杀)**。具体思路如下:
1、利用redis的watch功能,监控这个redisKey的状态值 2、获取redisKey的值,创建redis事务,给这个key的值+1 3、执行这个事务,如果key的值被修改过则回滚,key不加1
利用setnx防止库存超卖 分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 利用Redis的单线程特性对共享资源进行串行化处理
1 | // 获取锁推荐使用set的方式 |
1 | // 释放锁,非原子操作,可能会释放其他线程刚加上的锁 |
分布式锁存在的问题:
- 客户端长时间阻塞导致锁失效问题
计算时间内异步启动另外一个线程去检查的问题,这个key是否超时,当锁超时时间快到期且逻辑未执行完,延长锁超时时间。
**Redis服务器时钟漂移问题导致同时加锁 redis的过期时间是依赖系统时钟的,如果时钟漂移过大时 理论上是可能出现的 **会影响到过期时间的计算。
单点实例故障,锁未及时同步导致丢失
RedLock算法
获取当前时间戳T0,配置时钟漂移误差T1
短时间内逐个获取全部N/2+1个锁,结束时间点T2
实际锁能使用的处理时长变为:TTL - (T2 - T0)- T1
该方案通过多节点来防止Redis的单点故障,效果一般,也无法防止:
主从切换导致的两个客户端同时持有锁
大部分情况下持续时间极短,而且使用Redlock在切换的瞬间获取到节点的锁,也存在问题。已经是极低概率的时间,无法避免。Redis分布式锁适合幂等性事务,如果一定要保证安全,应该使用Zookeeper或者DB,但是,性能会急剧下降。
与zookeeper分布式锁对比
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
- zk 分布式锁,注册个监听器即可,不需要不断主动尝试获取锁,ZK获取锁会按照加锁的顺序,所以是公平锁,性能和mysql差不多,和redis差别大
Redission生产环境的分布式锁
Redisson是基于NIO的Netty框架上的一个Java驻内存数据网格(In-Memory Data Grid)分布式锁开源组件。
[
- 客户端请求加锁
- 客户端1请求加锁:客户端1通过 Redisson 向 Redis 发送加锁请求。
- 是否加锁:Redisson 会检查是否能够成功获取锁。如果能获取锁,则进入下一步,否则根据哈希算法选择一个节点进行加锁。
- 加锁过程
- 根据哈希算法选择一个节点:Redisson 根据哈希算法选择一个 Redis 主节点来处理加锁请求。
- 执行Lua脚本加锁:加锁操作通过 Lua 脚本在 Redis 主节点上执行,确保操作的原子性。
- 看门狗机制
- 看门狗定时续约:一旦客户端1成功获取锁,Redisson 的看门狗机制启动。看门狗每隔10秒检查锁的状态,如果客户端还持有锁,则会自动延长锁的生存时间,防止锁过期。
- 客户端释放锁
- 释放锁和删除 key:当客户端1完成任务后,会主动释放锁并删除对应的 Redis 键。
- 另一个客户端请求加锁
- 客户端2请求加锁:客户端2通过 Redisson 发送加锁请求。
- 是否加锁:Redisson 会检查是否能够成功获取锁。如果不能成功获取锁,客户端2会进入一个循环,不断尝试获取锁,直到成功。
Redis 集群
- Redis 集群架构:Redis 集群由多个主从节点组成,主节点负责写操作,从节点负责读操作和数据复制。
但当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景(重复下单,重复转账),请不要使用redis分布式锁。可以使用CP模型实现,比如:zookeeper和etcd。
Redis | zookeeper | etcd | |
---|---|---|---|
一致性算法 | 无 | paxos(ZAB) | raft |
CAP | AP | CP | CP |
高可用 | 主从集群 | n+1 | n+1 |
实现 | setNX | createNode | restfulAPI |
6、redis心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送ACK命令:
1、检测主从的连接状态 检测主从服务器的网络连接状态
lag的值应该在0或1之间跳动,如果超过1则说明主从之间的连接有 故障。
2、辅助实现min-slaves,Redis可以通过配置防止主服务器在不安全的情况下执行写命令
1 | min-slaves-to-write 3 (min-replicas-to-write 3 ) |
上面的配置表示:从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10 秒时,主服务器将拒绝执行写命令。
3、检测命令丢失,增加重传机制
如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发 送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量, 然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。
Redis实战
1、Redis优化
读写方式 简单来说就是不用keys等,用range、contains之类。比如,用户粉丝数,大 V 的粉丝更是高达几千万甚至过亿,因此,获取粉丝列表只能部分获取。另外在判断某用户是否关注了另外一个用户时,也只需要关注列表上进行检查判断,然后返回 True/False 或 0/1 的方式更为高效。
KV size 如果单个业务的 KV size 过大,需要分拆成多个 KV 来缓存。拆分时应考虑访问频率
key 的数量 如果数据量巨大,则在缓存中尽可能只保留频繁访问的热数据,对于冷数据直接访问 DB。
读写峰值 如果小于 10万 级别,简单分拆到独立 Cache 池即可 如果达到 100万 级的QPS,则需要对 Cache 进行分层处理,可以同时使用 Local-Cache 配合远程 cache,甚至远程缓存内部继续分层叠加分池进行处理。(多级缓存)
命中率 缓存的命中率对整个服务体系的性能影响甚大。对于核心高并发访问的业务,需要预留足够的容量,确保核心业务缓存维持较高的命中率。比如微博中的 Feed Vector Cache(热点资讯),常年的命中率高达 99.5% 以上。为了持续保持缓存的命中率,缓存体系需要持续监控,及时进行故障处理或故障转移。同时在部分缓存节点异常、命中率下降时,故障转移方案,需要考虑是采用一致性 Hash 分布的访问漂移策略,还是采用数据多层备份策略。
过期策略
可以设置较短的过期时间,让冷 key 自动过期;也可以让 key 带上时间戳,同时设置较长的过期时间,比如很多业务系统内部有这样一些 key:key_20190801。
缓存穿透时间 平均缓存穿透加载时间在某些业务场景下也很重要,对于一些缓存穿透后,加载时间特别长或者需要复杂计算的数据,而且访问量还比较大的业务数据,要配置更多容量,维持更高的命中率,从而减少穿透到 DB 的概率,来确保整个系统的访问性能。
缓存可运维性 对于缓存的可运维性考虑,则需要考虑缓存体系的集群管理,如何进行一键扩缩容,如何进行缓存组件的升级和变更,如何快速发现并定位问题,如何持续监控报警,最好有一个完善的运维平台,将各种运维工具进行集成。
缓存安全性 对于缓存的安全性考虑,一方面可以限制来源 IP,只允许内网访问,同时加密鉴权访问。
2、Redis热升级
在 Redis 需要升级版本或修复 bug 时,如果直接重启变更,由于需要数据恢复,这个过程需要近 10 分钟的时间,时间过长,会严重影响系统的可用性。面对这种问题,可以对 Redis 扩展热升级功能,从而在毫秒级完成升级操作,完全不影响业务访问。
热升级方案如下,首先构建一个 Redis 壳程序,将 redisServer 的所有属性(包括redisDb、client等)保存为全局变量。然后将 Redis 的处理逻辑代码全部封装到动态连接库 so 文件中。Redis 第一次启动,从磁盘加载恢复数据,在后续升级时,通过指令,壳程序重新加载 Redis 新的 redis-4.so 到 redis-5.so 文件,即可完成功能升级,毫秒级完成 Redis 的版本升级。而且整个过程中,所有 Client 连接仍然保留,在升级成功后,原有 Client 可以继续进行读写操作,整个过程对业务完全透明。