一、NoSQL和RDBMS的区别
传统的rdbms
- 结构化组织
- SQL
- 数据和关系都存储在单独的表中
- 操作语言是数据库定义语言
- 严格的一致性
- 基础的事务
NoSql
- 不仅仅是数据
- 没有固定的语言
- 键值对存储,列存储、文档存储、图形数据库
- 最终一致性
- cpa 定理 和base
- 高性能,高可用,高可扩
二、NoSql的四大分类
1、KV键值对:redis
2、文档型数据库
- MongoDB
-
- 基于分布式文件存储的数据库,主要用来处理大量文档
- 是一个介于关系型和非关系型之间的中间产品,功能最丰富,最像关系型数据库
3、列存储数据库
- HBase
- 分布式文件系统
4、图关系数据库
- 存放的是关系,如:朋友圈社交网络,广告推荐
- InfoGrid、Neo4j
三、redis
1、redis是什么?
Redis (Remote Dictionary Server) 远程字典服务
是一个开源的支持网络、可基于内存也可持久性的日志型、key-value数据库,提供多种语言的API
2、Redis的作用
- 内存存储、持久化(RDB和AOF)
- 效率高,可以用于高速缓存
- 发布订阅系统,队列
- 地图信息分析
- 计数器、计时器(如:浏览量)
3、Redis的特征
- 多样化的数据类型
- 持久化
- 集群
- 事务
4、Redis性能测试
redis-benchmark 是官网自带的压力测试工具
四、Redis 的八种数据类型(指的是value)
Redis的五种基本数据类型
- String 用于存储整型、字符串类型的数据
-
- 应用场景:计数器、粉丝数、对象缓存存储
- List (列表)可以用作栈、队列、阻塞队列
-
- 实际上是一个链表,前后都可以插入
- 如果key不存在,就创建新的链表
- 空链表也代表不存在
- 在两边插入或者改动值,效率最高
- set集合
-
- 支持差集、交集、并集操作
- Hash 也是key-value形式,但是value是一个map
-
- 适合存储经常变动的对象信息,String更适合存储字符串
- ZSet 排序的set集合
Redis的三种特殊的数据类型
- geospatial (geo)
-
- 3.2推出,用于推算出地理位置信息,两地之间的距离
- hyperloglog 基于基数统计的算法
-
- 基数 数学上集合元素的个数 不能重复
- hyperloglog的优点是占用内存小,并且是固定的,存储2^64个不同元素的基数,只要12k的空间
- 常用于统计网站的UV,传统的方式是set保存用户的ID,然后统计set中元素的数量作为判断标准,这种方式保存了大量的用户ID,占用大量空间,hyperloglog就比较合适了
- bitmap (位图)
-
- 通过最小的单位bit来进行0或1 的设置,表示某个元素对应的值或者状态。一个bit的值,要么0要么1,也就是说bit能存储的最多信息是2
- 常用于统计用户信息,比如活跃粉丝和不活跃粉丝,登录和未登录,是否打卡
五、Redis的事务和监视
监视 watch
乐观锁:认为什么时候都不会出现问题,所以不上锁。更新数据的时候会判断一下,在此期间是否修改过监视的数据
redis事务中watch的作用,watch命令可以监控一个或者多个key,一旦其中一个被修改,之后的事务就不会再执行。监控一直持续到exec命令。假设我们通过watch命令在事务执行之前监控了多个key,在watch之后又任何key值发生变化。exec命令执行的事务都会被放弃,同时返回Null mutil-bulk应答以后通知调用者事务执行失败。
所以,watch监控key后再去操作这些键,否则watch可能会起不到作用
六、Redis持久化
redis是内存数据库,如果不将内存中的数据库状态保存到磁盘中,那么一旦服务器退出,服务器中的数据库状态就会消失,所以Redis提供了持久化功能
1、RDB持久化机制(默认)
用数据集快照的方式记录 Redis 数据库的所有键值对,在某个时间点写入一个临时文件,持久化结束后,用这个临时文件替换上一次持久化的文件,达到数据恢复的目的。
优点:
- 只有一个.rdb文件 方便持久化
- 容灾性好,一个文件可以保存到安全的磁盘
- 性能最大化,Redis会单独创建(fork)一个子进程进行持久化,主进程不进行任何io操作保证了性能
- 在数据较多的时候,比AOF 的启动效率高,适合大规模数据恢复,对数据的完整性要求不高
缺点: 最后一次持久化的数据可能丢失,fork 进程的时候会占用一定的空间
2、AOF 持久化
是以独立日志的方式记录每次写命令,并在Redis重启的时候执行AOF文件中的命令以达到恢复数据的目的,AOF主要解决数据持久化的实时性。
优点:
- 数据安全,配置appendfsync 属性,可以选择不同的同步策略
- 数据的完整性更好
- 自动修复功能,redis-check--aof可以解决数据一致性的问题
缺点:
- AOF文件比 RDB文件大,且恢复数据速度慢
- 数据多,效率低于RDB
3、性能建议
- 因为 RDB 文件只用作后备用途,建议只在 Slave 上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留save 900 1 这条规则。
- 如果 Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒的数据,启动脚本较简单只 load 自己的 AOF 文件就可以了,代价是一是带来了持续的IO,二是 AOF rewrite 的最后将rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值是 64M 太小了,可以设置到 5G 以上,默认值超过原大小 100% 时重写,可以改到适当的数值。
- 如果不 Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用也可以,能省掉一大笔 IO ,也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时宕掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件,载入较新的那个,微博就是这种架构
4、持久化配置
默认情况下是rdb持久化,要选择aof,需要在redis.conf配置文件中修改配置
查看redis.conf所在目录的命令 find / -name redis.conf
vim进入redis.conf配置文件中
七、发布订阅模式、主从复制、哨兵模式
1、Redis订阅发布(pub/sub)
是一种消息通信模式:发送者发送(pub)消息,订阅者(sub)接受消息,Redis客户端可以订阅任意数量的频道
使用场景:
-
- 实时消息系统
- 实时聊天
- 订阅关注系统
- 稍微复杂的场景更多使用消息中间件MQ
2、主从复制
(1)概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称之为主节点(master),后者称之为从节点(slave),数据的复制都是单向的,只能从主节点到从节点。Master以写为主,Slave以读为主。
默认情况下,每台Redis服务器都是主节点。且一个主节点可以有多个从节点或者没有从节点,但是每个从节点只能有一个主节点
(2)主从复制的作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复:当主节点出现问题的时候,可以由从节点提供服务,实现快速的故障恢复。实际上也是一种服务的冗余
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个节点分担读负载,可以很大提高Redis服务器的并发量
- 高可用(集群)的基石,主从复制还是哨兵模式和集群能够实施的基础。
(3)Redis不会只使用一台,原因是
- 从结构上,单个Redis会发生单点故障,并且一台服务器需要处理所有的请求,压力很大
- 从容量上,单个Redis服务器内存有限,单台Redis服务器最大使用内存不应该超过20G
电商平台的商品,一般都是一次上传多次浏览,也就是读多写少,应该采用主从复制的架构
(4)复制原理
Master 接收到命令后,启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕后,master 将传送整个数据文件到 slave ,并完成一次完全同步。
同步的数据 使用的是RDB文件,收到同步命令之后,master会bgsave 把数据保存在rdb中,发送给从机。
bgsave:fork出来一个子进程 进行处理,不影响主进程的使用。
**全量复制:**Slave 服务在接收到数据库文件后,将其存盘并加载到内存中。
**增量复制:** Master 继续将新的所有收集到的修改命令一次传给 slave,完成同步。
但是只要重新连接 master ,一次完全同步(全量复制)将被自动执行。我们的数据一定可以在从机中看到。
3、哨兵模式
(1)概述
主从切换技术的方式是:当主机服务器宕机之后,需要手动将一台服务器切换为主服务器,这需要人工干预,费时费力,还会造成一段时间内的服务不可用。这不是一种推荐的方式,更多的时候我们优先考虑的的是哨兵模式。Redis 从 2.8 开始正式提供了 Sentinel(哨兵)架构来解决这个问题。
哨兵模式能够后台监控主机是否故障,如果故障了根据投票数(投哨兵节点)自动将从库转换为主库。
当有多个哨兵节点的时候,需要选出一个哨兵节点 来去进行主从切换。
哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它独立运行。其原理是**哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例**。
(2)哨兵的作用
- 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器
- 当哨兵检测到 master 宕机,会自动将 slave 切换为 master,然后通过发布订阅模式通知其他的从放服务器,修改配置文件,让他们切换主机。
然而一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机了,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵 1 主观认为主服务器不可用,这个现象称之为**主观下线**。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,这个过程称之为**客观下线**,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,最终会选出来一个leader(哨兵节点),进行 failover 【故障转移】。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主从切换。
(3)哨兵模式的优缺点
哨兵模式的优点
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
2、主从可以切换,故障可以转移,系统的可用性就会更好
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮。
哨兵模式的缺点
1、Redis 不方便在线扩容,集群达到一定的上限,在线扩容就会十分麻烦;
2、实现哨兵模式的配置其实也很麻烦,里面有甚多的配置项。
4、Redis集群--Redis Cluster
随着业务系统功能、模块、规模、复杂性的增加,我们对Redis的要求越来越高,尤其是在高低峰场景的动态伸缩能力,比如:电商平台平日流量较低且平稳,双十一大促流量是平日的数倍,两种情况下对于各系统的数量要求必然不同。如果始终配备高峰时的硬件及中间件配置,必然带来大量的资源浪费。
Redis作为业界优秀的缓存产品,成为了各类系统的必备中间件。哨兵模式虽然优秀,但由于其不具备动态水平伸缩能力,无法满足日益复杂的应用场景。在官方推出集群模式之前,业界就已经推出了各种优秀实践,比如:Codis、twemproxy等。
为了弥补这一缺陷,自3.0版本起,Redis官方推出了一种新的运行模式——Redis Cluster。
Redis Cluster采用无中心结构,具备多个节点之间自动进行数据分片的能力,支持节点动态添加与移除,可以在部分节点不可用时进行自动故障转移,确保系统高可用的一种集群化运行模式。按照官方的阐述,Redis Cluster有以下设计目标:
- 高性能可扩展,支持扩展到1000个节点。多个节点之间数据分片,采用异步复制模式完成主从同步,无代理方式完成重定向。
- 一定程度内可接受的写入安全:系统将尽可能去保留客户端通过大多数主节点所在网络分区所有的写入操作,通常情况下存在写入命令已确认却丢失的较短时间窗口。如果客户端连接至少量节点所处的网络分区,这个时间窗口可能较大。
- 可用性:如果大多数节点是可达的,并且不可达主节点至少存在一个可达的从节点,那么Redis Cluster可以在网络分区下工作。而且,如果某个主节点A无从节点,但是某些主节点B拥有多个(大于1)从节点,可以通过从节点迁移操作,把B的某个从节点转移至A。
简单概述。结合以上三个目标,我认为Redis Cluster最大的特点在于可扩展性,多个主节点通过分片机制存储所有数据,即每个主从复制结构单元管理部分key。
因为在主从复制、哨兵模式下,同样具备其他优点。
当系统容量足够大时,读请求可以通过增加从节点进行分摊压力,但是写请求只能通过主节点,这样存在以下风险点:
- 所有写入请求集中在一个Redis实例,随着请求的增加,单个主节点可能出现写入延迟。
- 每个节点都保存系统的全量数据,如果存储数据过多,执行rdb备份或aof重写时fork耗时增加,主从复制传输及数据恢复耗时增加,甚至失败;
- 如果该主节点故障,在故障转移期间可能导致所有服务短时的数据丢失或不可用。
所以,动态伸缩能力是Redis Cluster最耀眼的特色。
(1)哈希槽
Redis-cluster引入了哈希槽的概念。
Redis-cluster中有16384(即2的14次方)个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。
Cluster中的每个节点负责一部分hash槽(hash slot)。
比如集群中存在三个节点,则可能存在的一种分配如下:
- 节点A包含0到5500号哈希槽;
- 节点B包含5501到11000号哈希槽;
- 节点C包含11001 到 16383号哈希槽。
(2)请求重定向
Redis cluster采用去中心化的架构,集群的主节点各自负责一部分槽,客户端如何确定key到底会映射到哪个节点上呢?这就是我们要讲的请求重定向。
在cluster模式下,节点对请求的处理过程如下:
- 检查当前key是否存在当前NODE?
-
- 通过crc16(key)/16384计算出slot
- 查询负责该slot负责的节点,得到节点指针
- 该指针与自身节点比较
- 若slot不是由自身负责,则返回MOVED重定向
- 若slot由自身负责,且key在slot中,则返回该key对应结果
- 若key不存在此slot中,检查该slot是否正在迁出(MIGRATING)?
- 若key正在迁出,返回ASK错误重定向客户端到迁移的目的服务器上
- 若Slot未迁出,检查Slot是否导入中?
- 若Slot导入中且有ASKING标记,则直接操作
- 否则返回MOVED重定向
move重定向:
- 槽命中:直接返回结果
- 槽不命中:即当前键命令所请求的键不在当前请求的节点中,则当前节点会向客户端发送一个Moved 重定向,客户端根据Moved 重定向所包含的内容找到目标节点,再一次发送命令。
ASK 重定向:
Ask重定向发生于集群伸缩时,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用Ask重定向来解决此种情况
八、缓存穿透、缓存击穿、缓存雪崩(解决方案中的红色是我用过的)
缓存穿透
描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;实际上我们一般会缓存空值 |
缓存击穿
描述:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
1、设置热点数据永远不过期。
2、接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务 不可用时候,进行熔断,失败快速返回机制。
3、布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,
4、加互斥锁
分布式锁:使用分布式锁,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因对分布式锁的考验很大。
缓存雪崩
描述:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
3. 设置热点数据永远不过期。
- 高可用集群
更新:高并发场景下,用redis作为缓存中间件来提高性能,吞吐量和系统响应时间都会有显著的提升
Redis在使用过程中遇到的问题及解决方案:缓存失效1-3
1、缓存穿透:查询一个不存在的数据,redis和mysql中都没有,当大批量的请求都达到数据库,会导致mysql压力过大宕机,缓存在这时候是失去意义的,【解决方案】把空结果缓存在redis中,并设置过期时间,这样再有请求访问这条空数据就拦截在缓存层了,后续有真实数据了,由于对空结果设置了过期时间,也不会影响再将真实数据放入缓存。
2、缓存击穿:针对同一个key,比如热点key缓存过期,大量请求同时访问该热点key,那么所有的查询都会打到数据库,导致数据库压力过大。【解决方案】:1)热点数据永不过期,2)加锁,大量并发只让一个人去查,查到后就会把数据加入缓存,释放锁,其他人拿到锁后就会先去查缓存,缓存中已经有数据了,就不会去查数据库。
3、缓存雪崩:缓存中大批量数据同时失效,导致查询这些同时失效的数据的 请求打到数据库,以至于数据库压力过大雪崩。【解决方案】:在原有失效时间的基础上,加一个随机值,这样每一个缓存的过期时间重复率就会降低,很难引发大面积失效的问题。
九、Redis面试题总结
9.1 什么是Redis?有什么特点
Redis 是一款开源,高性能的 key-value 的非关系型数据库。内存数据库
特点:
1)支持持久化,可以将内存中的数据持久化到磁盘,重启可以再次从磁盘中加载使用;
2)支持多种数据结构;
3)支持数据的备份:主从模式的备份;支持集群
4)高性能,读速度达 11 万次/秒,写速度达到 8.1 万次/秒
5)支持事务
9.2 说说 Redis 的数据类型
一共 8 种
5 种基本数据类型:String、Hash、List、Set、Zset
3 种特殊类型:geospatial、hyperloglog、bitmap
9.3 Redis 和 Memcache 的区别?
1)Memcache 数据都存储在内存中,断电即失,数据不能超过内存大小;而 Redis 的数据可以持久化到硬盘。
2) Memcache 只支持简单的字符串,Redis 有丰富的数据结构;
3)底层实现方式不一样,Redis 自行构建了 VM 机制,速度更快。
9.4 Redis 是单线程的?
Redis 将数据放在内存中,单线程执行效率最高,多线程执行反而需要进行 CPU 上下文切换,这是个耗时操作,单线程效率最高。
9.5 说说 Redis的持久化
Redis 提供了两种持久化机制:RDB 和 AOF
RDB 持久化机制指的是,用数据集快照的方式记录 Redis 数据库的所有键值对,在某个时间点写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复的目的。
优点:
1)只有一个文件 dump.rdb 方便持久化;
2)容灾性好,一个文件可以保存到安全的磁盘;
3)性能最大化,Redis 会单独创建(fork)一个子进程进行持久化,主进程不进行任何 IO 操作,保证了性能;
4)在数据较多时,比 AOF 的启动效率高。
缺点:
最后一次持久化的数据可能会丢失。
AOF 持久化,是以独立日志的方式记录每次写命令,并在 Redis 重启时重新执行 AOF 文件中的命令以达到恢复数据的目的。AOF 主要解决数据持久化的实时性。
优点:
1)数据安全,配置 appendfsync 属性,可以选择不同的同步策略;
2)自动修复功能, redis-check-aof工具可以解决数据一致性问题;
缺点:
1)AOF 文件比 RDB 文件大,且恢复速度慢;
2)数据多时,效率低于 RDB。
AOF如何防止文件过大?AOF重写,只保留最后一次的修改记录。
9.6 Redis 的主从复制
主从复制值的是将一台 Redis 服务器的数据复制其他 Redis 服务器,前者称之为主节点,后者称之为从节点。
主从复制的作用:
1)数据冗余:主从复制实现了数据的热备份;
2)故障修复:当主节点出现故障后,从节点还可以提供服务,实现快速的故障修复。
3)负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写操作,从节点提供读操作,实现负载均衡,提高并发量;
4)高可用的基石:主从复制是哨兵模式的基础。
复制原理:
从节点启动成功连接到主节点后,会发送一个 sync 的同步命令。主节点接收到命令之后,启动后台的存盘进程,收集所有修改数据库的命令,在后台执行完毕后将整个数据文件传送到从节点,完成一次完全同步。
全量复制:从节点在接收到了数据文件后,将其存盘文件加载都内存中;
增量复制:主节点继续将新收集到修改命令传递给从节点,完成同步
9.7 说说哨兵模式
哨兵模式是为了解决手动切换主节点的问题。Redis 提供了哨兵的命令,哨兵是一个独立的进程。哨兵能够后台监控主节点是否故障,如果故障需要将从节点选举为主节点。
其原理是哨兵通过发送命令,等待 Redis 服务器的响应,从而监控多个 Redis 节点。
当只有一个哨兵时,还是可能会出现问题的,比如哨兵自己挂掉。为此,可以使用多哨兵模式,多个哨兵之间相互监控。当主节点宕机了,哨兵1先检测到这个结果,系统并不会马上进行 failover 【故障转移】的过程。仅仅是哨兵1认为主节点不可用的现象称之为主观下线。当其余的哨兵也检测到主节点不可用之后,哨兵之间会进行一次投票选举从节点中的一个作为新的主节点,这个过程称之为客观下线。
哨兵模式的优点:
1)基于主从复制,高可用;
2)主从可以切换,进行故障转移,系统可用性好;
3)哨兵模式是主从模式的升级版,手动到自动,更加健壮。
哨兵模式的缺点:
1)不方便在线扩容;
2)实现哨兵模式需要很多的配置。
9.8 缓存穿透、缓存击穿和缓存雪崩
缓存穿透:
概念:用户需要查询一个数据,缓存中没有,也就是没有命中,于是向数据库中发起请求,发现也没有。当用户很多的时候,缓存都没有命中,于是都去请求数据库,这给数据库造成很大的压力。
解决方案:
- 布隆过滤器:是一种数据结构,对所有可能查询的参数以 hash 方式存储,先在控制层进行校验,不符合则丢弃,避免了过多访问数据库。
- 缓存空对象:当存储层没有命中时,即使返回空对象也将其缓存起来。(意味着更多的空间存储,即使设置了过期时间,缓存和数据库还是有段时间数据不一致。)
缓存击穿:
概念:当一个 key 非常热点时,在不断扛高并发,集中对这个热点数据进行访问,当这个 key 失效的瞬间,请求直接到达数据库,给数据库瞬间的高压力。
解决方案:
- 设置热点数据永不过期
- 加分布式锁:保证每个 key 同时只有一个线程去查询后端服务。
缓存雪崩:
概念:某个时间段,缓存集中失效
解决方案:
- 增加 Redis 集群的数量
- 缓存过期时间的时候,错峰设置
- 限流降级:在缓存失效后,通过加锁和队列来控制数据库写缓存的线程数量
- 数据预热:正式部署之前,将数据预先访问一遍,让缓存失效的时间尽量均匀
9.9 Redis 的使用场景
1)会话缓存:如 单点登录,使用 Redis 模拟 session,SSO 系统生成一个 token,将用户信息存到 Redis 中,并设置过期时间;
2)全页缓存
3)作为消息队列平台
4)排行榜和计数器
5)发布/订阅:比如聊天系统
6)热点数据:比如ES中搜索的热词
9.10 Redis 缓存如何保持一致性
读数据的时候首先去 Redis 中读取,没有读到再去 MySQL 中读取,读取到数据更新到 Redis 中作为下一次的缓存。
写数据的时候会产生数据不一致的问题。无论是先写入 Redis 再写入 MySQL 中,还是先写入 MySQL 再写入 Redis 中,这两步操作都不能保证原子性,所以会出现 Redis 和 MySQL 中数据不一致的问题。
无论采取何种方式都不能保证强一致性,如果对 Redis 中的数据设置了过期时间,能够保证最终一致性,对架构的优化只能降低发生的概率,不能从根本上避免不一致性。
更新缓存的两种方式:删除失效缓存、更新缓存
更新缓存和数据库有两种顺序:先数据库后缓存、先缓存后数据库
两两组合,分为四种更新策略。
更新:
- 缓存数据一致性:
-
- 双写模式:写完数据库,把缓存中的数据也修改一下。会出现暂时性脏数据的问题,比如两个线程都写缓存,1号线程在写完数据库后由于卡顿等原因,还没有写缓存,2号线程已经完成了写数据库和写缓存的操作,此时,最新数据是2号线程更新的,但是1号线程后面又写了一次缓存,这时缓存中的数据依旧不是最新的数据,但是缓存过期后,会再次查询数据库更新缓存,最终也会得到最新的数据。
-
-
- 解决方案1、加锁,对写数据库和写缓存的操作加一个锁,都写完成后再释放锁
- 解决方案2、如果业务允许出现暂时性的脏数据,就不需要进行优化。
-
-
- 失效模式:更新数据库后,删除缓存,下次再查询数据的时候先查数据库再更新缓存。也会出现脏数据问题:比如有三个线程,1号线程写数据删缓存,二号线程也要写数据删缓存,在2号线程正在写数据还没有删缓存的时候,3号线程执行读缓存,这时候缓存为空,就会查数据库后读到更新缓存,此时更新到缓存中的数据还是1号线程写的数据,而不是最新的2号线程写的数据
- 缓存数据一致性总结:无论是双写模式还是失效模式,都会出现暂时性的数据不一致问题,即多个实例同时更新时会出现脏数据。
- 用户维度的数据(订单数据和用户数据),这种并发几率特别小,不需要考虑这个问题,设置好缓存过期时间,每隔一段时间主动更新即可
- 对于实时性、一致性要求高的数据,直接去查数据库就好。
- 我们放入缓存中的数据本来就不应该是对实时性、一致性要求超高的数据,所以缓存数据的时候加上过期时间,保证最终一致性即可
- 通过加锁保证并发读写,写写的时候需要顺序排队,读读相当于无锁,读写可以解决上述模式中的问题(写数据的时候,写锁不释放,不允许读,就可以保证缓存中存在的就是最新数据)
- 缓存数据+过期时间,可以解决大部分业务对缓存的需求
- 如果是菜单和商品介绍 等基础信息,可以使用canal订阅binlog的方式
9.11 集群
redis cluster 分为 16384个槽,最多可支持1000个节点,数据存放时,根据CRC16(key)%16384取模,得到对应的槽,存放到槽对应的节点上。
取数据时,会计算槽,进行请求重定向,如果在当前节点,直接返回数据,不在move到对应节点,获取数据
追加两个问题:
堆外内存溢出 OutOfDirectMemoryError,产生原因:Spring2.0以后默认使用Lettuce作为操作Redis的客户端,Lettuce使用netty进行网络通信。Lettuce的bug使netty堆外内存溢出,netty如果没有指定堆外内存,默认使用的是jvm设置的最大堆内存大小-Xmx, 可以通过 Dio.netty.maxDirectMemory 进行设置 netty在底层会进行计数 统计内存的使用量,超出最大容量限制就会报OutOfDirectMemoryError 操作完后会调用减内存的使用量,出现这个问题的原因就是Lettuce在某一步操作的时候没有及时进行减内存的操作。
解决方案:不能只通过Dio.netty.maxDirectMemory调大堆外内存,1)、升级Lettuce 2)、切换使用jedis
注:redisTemplete 操作的是RedisConnectionFactory,无论是Lettuce还是Jedis都会生成这个连接工厂
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion></exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
redis集群及新增节点的步骤:
- 准备新节点:确保有一个新的Redis实例已经安装并且可以单独运行。
- 配置节点:确保新节点的配置文件(通常是redis.conf)中的端口号、集群配置等与集群中其他节点不冲突。
- 启动新节点:使用redis-server命令启动新的Redis实例。
- 将新节点加入集群:使用
redis-cli --cluster add-node
命令将新节点加入到现有的集群中。 - 一旦新节点成功加入到集群中,Redis会自动开始数据的迁移和复制过程。在这个过程中,新节点将从现有节点中同步数据,并确保数据的完整性和一致性。
- 当数据迁移完成后,需要执行重新平衡操作,确保数据在集群中的分布均匀。可以使用以下命令进行数据的重新平衡:redis-cli --cluster rebalance
附:Redisson分布式锁
先简述本地锁在分布式系统中的不足:
本地锁,在查询数据库的方法上或者代码块加synchronized锁,其中一个线程拿到锁之后,其他线程就会被阻塞,不会再查数据库,因此拿到锁的这个线程查到数据之后要把数据再加入到缓存中,这样其他线程拿到锁后会再一次去缓存中确认是否有数据,可以查到数据了,就不需要再查询数据库。但是方法上或者代码块加synchronized锁都是针对当前实例加的锁,也就是一个容器一个锁,而同一个微服务可能会有很多台机器,没太机器就都是分别加的锁,比如有十个订单系统的机器,synchronized(this)只会对当前机器加锁,其他请求访问其他机器的时候,还是会经过一次查询数据库的步骤,这样也可以解决缓存击穿的问题,并且性能上也不会有太大的影响,所以本地锁的缺点就是在分布式情况下,锁不住所有的服务。
分布式锁
- redis setnx命令实现分布式锁:占坑 set lock 1 ex(锁过期时间)nx(不存在才可以设置) 需要保证加锁和解锁的原子性,加锁set ex nx命令保证原子性,解锁需要使用lua脚本保证原子性,未抢占到锁的线程设置指定时间重试。 redisTemplete操作加锁解锁:redisTemplate.opsForValue().setIfPresent("lock", uuid, 1, TimeUnit.MINUTES);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
public void test(){
String uuid = UUID.randomUUID().toString();
//占坑
Boolean lock = redisTemplate.opsForValue().setIfPresent("lock", uuid, 1, TimeUnit.MINUTES);
if (lock){
System.out.println("获取到锁");
//执行业务逻辑
System.out.println("执行业务逻辑");
//保证原子性的删除锁,lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
} else {
//重试
try {
Thread.sleep(100);
}catch (Exception e) {
}
//自旋,重新获取锁
Thread.sleep(30000);
test();
}
/*//获取锁,未保证原子性
String lockValue = redisTemplate.opsForValue().get("lock");
if (uuid.equals(lockValue)){
//执行业务逻辑
System.out.println("执行业务逻辑");
//删除锁
redisTemplate.delete("lock");
}else {
//重试
test();
}*/
}
- Redisson 分布式锁的操作步骤:
-
- 导入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.35.0</version>
-
- 配置RedissonClient
/*
*所有对redisson的操作都是通过redissonClient对象
*
* */
@Bean(destroyMethod = "shutdown")
RedissonClient redisson() throws IOException {
//创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.232.129:6379");
//根据Config 创建RedissonClient实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
//官方文档中还有集群模式创建配置的方式
-
- 使用RedissonClient操作加锁解锁
-
-
- 分布式可重入锁(RLock),实现了juc包下的lock接口。可重入锁Reentrent Lock可以防止死锁出现,如A方法中调用B方法,A、B方法都加同一把锁,可重入锁情况下,执行A方法时就可以直接执行B方法,而B方法不需要申请锁。如若不然,A方法执行时,调B方法,此时B方法获取A方法的锁无法得到,A方法也就无法执行成功后释放锁,就会出现死锁的问题
-
//示例代码
//获取锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("myLock");
//加锁
// traditional lock method
lock.lock();//阻塞式等待,其他线程会在这里等待,直到拿到锁,Redisson不需要自旋
//1、锁的自动续期,如果业务超长,运行期间会自动给锁续上30s,不用担心业务超长,锁会到期删除
//2、加锁的业务只要运行完成,就不会再给锁续期,这样即使因为某种原因导致没有执行释放锁的代码,
//30s后也会自动释放锁
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);//自己指定锁的超时时间,要大于业务的执行时间
//问题:lock.lock(10, TimeUnit.SECONDS);在锁的时间到了之后,不会自动续期
//如果我们传递了锁的超时时间,就发送redis执行脚本,进行占锁,默认时间就是我们设置的超时时间
//如果我们为设置锁的超时时间,就会使用看门狗的默认时间30*1000;
// 只要占锁成功,就会启动一个定时任务【重新给锁设置超时时间,默认还是看门狗的默认时间】
//定时任务会每隔看门狗默认时间/3,执行一次续期操作
// or wait for lock aquisition up to 100 seconds
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
-
-
- 分布式公平锁(RLock):RLock fair = redisson.getFairLock("fair"); 其他步骤与可重入锁一样
- 分布式读写锁(RReadWriteLock):读写锁保证一定可以读到最新数据,写锁是互斥锁,同一时间只能有一个线程写;读锁是共享锁,没有写操作的时候,读锁相当于无锁状态;写锁没释放,读就必须等待 。 读写锁一定是搭配使用的,先写后读,读需要等待;先读后写,写也得等待,只要有写锁,线程都需要等待
-
/*
* 读写锁保证一定可以读到最新数据,写锁是互斥锁,同一时间只能有一个线程写,
* 读锁是共享锁,没有写操作的时候,读锁相当于无锁状态
* 写锁没释放,读就必须等待
* */
public String testWriteLock(){
RReadWriteLock rWlock = redisson.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = rWlock.writeLock();
try {
//改变数据加写锁,读取数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue", s);
System.out.println("执行业务逻辑");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
rWlock.writeLock().unlock();
}
return s;
}
public String testReadLock(){
RReadWriteLock rWlock = redisson.getReadWriteLock("RWlock");
String s = "";
RLock rLock = rWlock.readLock();
rLock.lock();
try {
s = redisTemplate.opsForValue().get("writeValue");
}catch (Exception e){
}finally {
rLock.unlock();
}
return s;
}
-
-
- 分布式信号量(RSemaphore):RSemaphore park = redisson.getSemaphore("park");
- 分布式闭锁(RCountDownLatch):RCountDownLatch countDownLatch = redisson.getCountDownLatch("countDownLatch");
-
-
- Redisson分布式锁的种类与JUC包下的本地锁基本一致,从理解上也基本一致,个人感觉区别还是本地锁只能锁住本实例,而分布式锁可以锁住所有实例,如下单场景中的超卖问题,我们希望同时只有一个线程可以下单这件商品,为这件商品锁定库存,而不会影响其他商品的下单,本地锁会导致每台机器都会有一个线程操作数据库,没有真正实现加锁的目的,分布式锁才可以实现。
- 接上述场景:分布式锁解决超卖问题不会影响库存充足时并发购买同一件商品的效率?
-
-
- 在分布式系统中,当多个用户或服务尝试同时购买同一件商品时,通过使用分布式锁,可以确保同一时间只有一个请求能够修改库存状态,从而避免超卖情况的发生。这种机制在库存充足时,能够确保并发购买的效率不受影响,同时保证每个购买请求都能得到正确处理。分布式锁的实现通常依赖于外部存储系统,如Redis,它提供了分布式锁的功能,可以确保在并发情况下,只有获得锁的请求才能进行库存扣减操作,而其他请求则会等待直到获得锁或者超时放弃。这种方式不仅保证了库存的正确性,也提高了系统的并发处理能力,使得在库存充足的情况下,用户能够快速完成购买,不会因为等待锁而导致购买流程延迟。
- 此外,通过使用乐观锁机制、Redis预减库存、消息队列确保顺序以及限制抢购频率等方法,可以进一步提高系统的并发处理能力和订单处理的准确性。这些措施共同作用,确保了在库存充足的情况下,用户能够快速完成购买,而不会因为分布式锁的使用而导致购买效率下降
-
官方文档:使用Redis setNX命令占锁 Commands | Docs
Redisson官网:Distributed Locks with Redis | Docs
标签:Redisson,数据,数据库,Redis,哨兵,缓存,小结,节点 From: https://blog.csdn.net/qq_43583691/article/details/142363169