Redis 是一个高性能的内存数据库,它支持多种数据结构,在底层实现中通过多种精心设计的数据结构来实现其高效的操作。
redis特点:
为什么用 Redis 缓存 MySQL:
Zset 类型的底层数据结构是由跳表实现的:
跳表(SkipList)是一种用来保持有序元素的数据结构,它通过多个层次的链表加速查找操作。简单来说,跳表是一个由多个链表构成的结构,每一层链表都比上一层少一些元素,允许通过跳跃的方式快速查找。
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理。是因为这些任务的操作都是很耗时的,很容易发生阻塞,这样就无法处理后续的请求了。
并且在后续发展中,网络I/O也会限制Redis的性能,为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。
因此在Redis6.0后,Redis 在启动的时候,默认情况下会额外创建 6 个线程(这里的线程数不包括主线程):
Redis 采用了 事件驱动模型,即使用一个单线程处理所有客户端连接的请求。这个线程会 循环监听 事件并响应相应的请求。
Redis 初始化的时候,会做下面这几件事情:
初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:
epoll_wait()
阻塞,等待事件。epoll
中,监听该连接的读写事件。recv()
会立即返回。send()
会立即返回。SETNX
不会做任何操作。SET
命令实现的带有超时的分布式锁
SET
命令的 NX 和 EX 参数,可以用来在一个命令中同时实现 设置键值、设置锁 和 设置超时 的功能。
redis 执行一条命令的时候是具备原子性的,因为 redis 执行命令的时候是单线程来处理的,不存在多线程安全的问题。
如果要保证 2 条命令的原子性的话,如何实现?
可以考虑用 lua 脚本
将多个操作写到一个 Lua 脚本中,Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。
redis 事务
INCR
)都是通过单线程执行的。这意味着当 Redis 处理一个客户端的请求时,它不会同时处理其他客户端的请求。这就天然保证了在执行 INCR
命令时不会被其他命令打断,从而实现原子性。在 Redis 分布式锁的场景中,时钟问题通常指的是Redis 服务器和客户端的时钟不同步,尤其是在锁的有效期(TTL)设置上。
原因:
客户端时钟漂移:客户端和 Redis 服务器之间的时钟可能存在差异。如果客户端使用的时钟比 Redis 服务器快或者慢,可能导致锁的过期时间错误。
Redis 服务器时钟漂移:如果 Redis 服务器的时钟出现漂移,可能会导致 Redis 锁过期时间的计算错误,导致锁失效或者提前释放。
网络延迟:在某些情况下,客户端和 Redis 之间的网络延迟可能会导致客户端计算的 TTL 时间与实际过期时间不一致。
解决方法:
Redisson
库SETNX
命令RDB(快照存储)
RDB 持久化是 Redis 默认的持久化机制,它会定期将 Redis 内存中的数据 快照 保存到磁盘上的一个 .rdb
文件中。
AOF(追加日志文件)
AOF 持久化方式通过将每个写操作都记录到一个日志文件中来实现持久化。每次客户端执行写操作时,Redis 会把操作记录在 AOF 文件中。
fsync
策略来控制何时同步到磁盘。fsync
配置,AOF 可以实现多种持久化方式,包括“每次写操作后同步”(最强一致性),以及“每秒同步一次”等。RDB 和 AOF 结合使用
maxmemory
限制时,就会触发内存淘汰机制,根据配置的策略删除一些数据。Redis 持久化文件有两种格式:RDB(Redis Database)和 AOF(Append Only File)
Redis 通过 主从复制 实现读写分离和基本的数据冗余,通过 哨兵机制 实现自动故障转移,而通过 集群模式 实现更高规模的分布式数据存储与高可用性。结合这些机制,Redis 能确保服务在节点或主服务器故障时保持可用。
主从复制(Replication)
原理:Redis 可以配置多个从服务器(Slave)来复制主服务器(Master)的数据。
高可用性:从服务器实时同步主服务器的数据,当主服务器出现故障时,可以将一个从服务器提升为主服务器,保证服务继续提供。
哨兵机制(Sentinel)
原理:Redis Sentinel 是一种监控和故障转移机制,用于监控 Redis 实例的健康状态,并在主服务器宕机时自动进行故障转移
高可用性:当 Sentinel 发现主服务器宕机时,它会自动选择一个从服务器提升为主服务器,并通知其他 Redis 实例进行更新。
集群模式(Cluster)
原理:Redis 集群允许采用哈希槽(Hash Slot)将数据分布到多个节点上,每个节点负责存储数据的某一部分(通过分片)。Redis 集群具有自动分片和数据冗余功能。
高可用性:集群模式支持每个数据分片有多个副本(主从复制),当某个节点发生故障时,集群会自动将该分片的副本提升为主节点。
群脑裂(Split Brain)是指在分布式系统中,多个节点出现网络分区或失去联系,导致系统分为多个不相互通信的“脑”,这些“脑”可能独立地做出决策,造成数据不一致或丢失。
使用 Redis Sentinel:监控主节点的健康,自动进行故障转移,确保主节点宕机时能自动选举新的主节点。
设置 cluster-require-full-coverage
为 yes
:这样在脑裂发生时,集群会停止接受写请求,避免数据不一致。
增加副本数量:每个分片配置多个副本,即使发生脑裂,仍有副本可用来恢复数据。
定期备份(RDB 和 AOF):发生脑裂导致的数据丢失时,可以从备份中恢复。
在redis集群下执行redis命令会根据key求哈希,确定具体的槽位(slot),然后将命令路由到负责该槽位的 Redis 节点上。
在redis集群下执行Lua需要保证Lua脚本中的所有key必须落到同个redis节点。
解决方法:
在从队列中取数据时先获取锁,获取锁成功从同步队列取出数据,数据处理完成释放锁。
因为队列中的数据量不确定,任务处理时间不确定,使用redisson的看门狗机制,默认锁的时间是30s,当到20s时自动续期.
一定要注意:获取锁要放在try中,在finally 中要释放锁。
null
或空集合),也将该结果缓存一段时间,避免后续相同请求每次都查询数据库。布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,主要用于测试一个元素是否属于一个集合。它的核心特点是能够快速判断某个元素是否存在于集合中,但有一定的误判率(假阳性),即可能会错误地报告某个元素存在,但不会漏掉实际存在的元素。
布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
1
。1
,说明元素“可能”存在;如果有位置是 0
,说明元素一定不存在。Cache Aside(旁路缓存)策略
读写策略
适用场景:Cache Aside 策略适合读多写少的场景,不适合写多的场景
缺点:存在缓存穿透的问题
Read/Write Through(读穿 / 写穿)策略
Write Back(写回)策略
保证缓存一致性需要根据具体的需求来定:
对数据实时性有一定要求
对数据实时性有一定要求即数据库数据更新需要近实时查询到最新的数据,针对这种情况可采用延迟双删、Canal+MQ异步同步的方式。
对数据实时性要求不高
使用定时任务的方式定时更新缓存,或者直接用redis查也行。
对数据实时性要求非常高
此类场景不适合用缓存,直接使用数据库即可
Redis 可以通过Zset来实现延迟队列。这个方法利用了有序集合的 按分数排序 特性来实现任务的延迟执行。
ttl+死信交换机怎么实现延迟队列?
TTL (Time-To-Live):指定消息在队列中存活的最大时间。当消息的 TTL 超过设置的时间后,消息就会被自动删除或转发到一个 死信交换机(DLX)。
delay_queue
。delay_queue
存活一段时间(TTL 到期后)。delayed_queue
获取到的消息就是经过延迟处理的。大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。
在Redis集群架构中对热Key进行复制。
使用读写分离架构。
用分布式锁、Redis等技术都可以防止超卖,Redisson 和 Lua 可以配合使用来增强 Redis 操作的性能和原子性。
现实使用Redisson实现分布式锁。使用多线程从同步队列查询并处理数据时,同一个队列只允许一个线程去处理,这里我们用到了分布式锁,锁的粒度是每个同步队列。
SET
命令加锁,要求锁的键不存在时才能设置,并且设置一个过期时间。DEL lock_key
删除锁,但为了避免误删其他客户端的锁,通常需要先确认锁的值是否和自己加锁时存的值一样,确保自己是持有锁的客户端。Redis 管道(Pipeline)是将多个命令打包一起发送到 Redis,避免每个命令都等待响应,从而减少网络延迟,提高性能。这样可以一次性执行多个命令,而不用每次等待一个命令的结果,适合批量操作。
不支持,要么全部成功要么全部失败,即使是lua脚本,也只是回到运行脚本之前的情况。
为了保证本地缓存和 Redis 缓存一致,可以采取以下几种策略: