Redis基本介绍
Remote Dictionary Server 高性能key-value数据库,支持BSD协议
Redis基本使用
数据类型
String
Redis使用SDS(Simple Dynamic String)动态字符串保存,可以根据不同的字符串长度使用不同的结构体
使用场景:
- 保存
Session
实现单点登录 - 计数器,记录网站的浏览量或者点赞数等信息,然后按照一定的规则持久化到数据库中
- 缓存数据,常用的是把缓存对象转为
Json
格式的字符串保存,读取的时候再反序列化
List 有序列表
Redis底层是使用QuickList
保存,相当于Java
中的LinkedList
,每个节点都是ZipList
的双向链表保存,所以从列表的两端取数据效率很高,查询数据的时间复杂度为O(n)
使用场景:
- 粉丝列表
- 文章评论列表等,可以使用lrange命令,进行分页查询,提高应用分页查询的速度
- 消息队列,使用LPush和BRPop命令可以实现简单的消息队列功能
常用命令
命令 | 作用 |
---|---|
LPUSH | 从列表头部插入一条数据 |
BRPOP | 从列表尾部移除一条数据,如果没有数据,则会一直阻塞等待到超时或者有元素为止 |
BRPOPLPUSH | 从列表中弹出一个值,并且将他插入到另外一个列表的头部,如果列表中没有元素,则会一直阻塞等待到超时或者有元素为止 |
RPOP | 从列表中取出指定下标的元素 |
Set 集合
内部使用value为空的Hashtable实现,查询的时间复杂度为O(1)
使用场景:
- 去重
SortedSet 有序集合
基于跳跃表实现,具体查看此文章,他给每一个元素设置一个score分数,然后根据分数进行排序。
使用场景:
- 各种排行榜
- 带权重的队列,可以让线程根据权重优先执行某些任务。
Hash
内部使用ziplist
或者Hashtable
实现,相当于Java
中的HashMap
,使用数据+链表的方式解决Hash冲突的问题
使用场景:
- 一般可以把
java
对象缓存到hash里面,然后通过key-value
的方式取值,但是实际开发中的对象一般比较复杂,嵌套类型,所以使用hash
较少
过期时间
Expire key seconds
给key
设置过期时间
删除策略
- 消极方法
- 当用户get获取值的时候,判断当前key是否过期,如果过期就删除,不返回给用户
- 积极方法
- 周期性的从设置了过期时间的key中随机的选择20个进行检查
- 删除已经过期的键
- 如果有25%的key过期,则重复一次该操作
pub/sub
publish channel message
发布一条消息到channel通道
subscribe channel[channel...]
订阅一个或者多个通道的消息
发布消息后就删除了,不能实现消息的持久化以及重发等功能,需要专门的消息队列中间件来实现(Kafka
、RocketMQ
、RabbitMQ
)
自增
使用incr
命令可以进行原子递增
getset
设置一个key的value并获取设置前的值,
Redis内存回收策略
- noeviction 不淘汰任何键值对,如果进行读操作则正常工作,进行写操作返回错误。Redis默认策略
- allkeys-lru 淘汰最近最少使用的键值对
- allkeys-random 对所有的键采用随机删除策略
- volatile-lru 在设置了过期时间的键中采用最近最少使用策略删除键
- volatile-random 在设置了过期时间的键中采用随机删除的策略删除键值对
- volatile-ttl 在设置了过期时间的键中,具有更早过期时间的key优先移除
JoinGroup过程
RDB方式:当一定的条件触发后,Redis会fork一个子进程来进行持久化操作,会把内存中的数据集以快照形式写入磁盘,采用二进制压缩存储,将所有的数据写入到一个临时文件中,等写入完成后会替换上次持久化的文件。
触发条件
- 用户配置(默认下面save seconds 操作次数)
- save 900 1
- save 300 10
- save 60 10000
-
bgsave
、save
调用save
方法或者bgsave
方法 -
flushall
清空数据 -
replication
主从同步数据
AOF: 每隔一秒或者每次更改redis
数据就将命令追加到AOF文件中
- 默认不开启,使用配置中
appendonly yes
打开AOF
-
AOF
文件体积过大时,会自动的在后台对AOF
进行重写,重写后新的AOF
文件包含了恢复当前数据集所需要的最小命令集;主进程会fork
一个子进程进行重写,类似于RDB
快照的方式。 -
auto-aof-rewrite-percentage
表示表示当前的AOF
文件大小 超过上一次重写时的AOF
文件大小的百分之多少时会再次进行重写,如果之前没有重写过,则以启动时AOF
文件大小为依据。 -
Auto-aof-rewrite-min-size
表示限制了允许重写的最小AOF
文件大小
RDB和AOF的优缺点:
- 当服务发生故障,
RDB
方式会丢失上次备份之后的所有数据,AOF
最多丢失1秒内的数据 -
Redis
重启时,首先通过RDB
加载数据到内存中,速度比较快,然后通过AOF
文件中的近期操作指令,将数据恢复到重启之前的状态 -
AOF
文件可读性比较好,但是相同内容的数据,会比RDB
大很多
Redis 单线程
Redis为什么这么块
- Redis是在内存中操作数据,而且存储的数据结构类似于
Java
中的HashMap
,查询的时间复杂度为O(1)
; - 单线程,不需要在线程间切换,减少了上下文切换的资源消耗,单线程也不需要考虑锁的使用,减少了获取锁和释放锁的资源消耗
- 使用I/O多路复用,同步非阻塞IO
Lua脚本
Redis
是单线程的,在内部不会存在线程安全的问题,但是如果有多个客户端同时访问,就相当于多线程,多个客户端之间没有请求的同步,实际顺序不一样就可能产生线程安全问题。使用Lua
脚本可以满足原子性。
基本使用
eval "redis.call(’set’,’hello’,’world')" 0
不带参数
eval “redis.call(’set’,KEYS[1],ARGV[1])” 1 hello world
带参数
Lua脚本的好处
- 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
- 原子操作,Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入
- 复用性,客户端发送的脚本会存储在Redis中,其他客户端可以复用这一脚本完成同样的逻辑。
Pinelining 管道
Redis
是一种基于客户端-服务端模型以及请求/响应协议的TCP
服务,通常会遵循以下步骤
- 客户端发送一个查询请求,并监听
socket
返回,通常是阻塞模式,等待服务端响应 - 服务端处理命令,然后将结果返回给客户端
如果客户端需要发送批量的命令的时候,往返时间就会变的很长(RTT Round Trip Time
),使用pipeline
可以减少RTT
的时间,服务端也可以见减少I/O的调用次数(用户态->内核态)
分布式锁的实现
setnx
命令可以设置一个key-value
键值对,如果当前Redis
中已经有该key
,会返回失败。多个进程同时进行setnx
操作的时候,只会有一个可以设置成功。
-
setnx
需要设置一个超时时间,防止获取锁的进程挂掉后导致死锁 - 存在一种情况,当一个客户端A获取锁成功后,由于某种原因阻塞了,然后超时时间到了,自动释放锁了,然后另一个客户端B获取了锁,此时客户端A阻塞结束并且运行结束后,会尝试释放锁,这时候可能会把客户端A的锁释放了。解决办法:
setnx key randomValue
设置一个随机值,每个客户端不同,释放锁的时候,首先判断一下当前value
是否与自己客户端相同,如果相同才能释放锁。
Redis集群
1.如何配置
master
节点不用做任何修改,只需要在slave
节点中,修改redis.conf
文件中添加slaveof master-ip master-port
2.数据如何同步
-
slave
初始化阶段,Redis
会触发全量复制,slave
需要将master
节点上的所有数据 - Master服务器执行
bgsave
命令(子线程),生成快照,同时记录在此期间的写命令,快照发送到Slave
节点并载入后,再把缓存的写命令发送过来执行命令 -
min-slaves-to-write 3
表示只有当3个slave同步完成,master才是可写的 -
min-slaves-max-lag 10
表示允许slave最长失去连接的时间是10秒,10秒还没收到slave
响应,master
就认为slave
已经断开 -
master node
会在内存中创建一个backlog
,master
和slave
都会保存要给replica offset
还有一个master id
,如果slave
断开连接,重连后slave会让master从上次的replica offset
开始继续复制数据,如果没有对应的offset
,则会进行一次全量同步
Redis哨兵(Sentinel)
Redis
集群之后,如果master
节点挂了,需要从slave
节点中选举master
,Redis
没有提供相关的功能,需要哨兵来进行监控。
哨兵是一个单独的进程,一般使用三个哨兵集群,保证哨兵的高可用。此时哨兵不仅会监控master
和slave
,还会互相监控。
配置:在redis-sentinel.conf文件中配置 sentinel monitor name ip port quorum,只需要配置master节点即可
哨兵之间相互感知
- 所有的
sentinel
向他们的监视的master
节点订阅channel
sentinel
- 新加入的
sentinel
节点向master
的这个节点发布一条消息,其他订阅了这个channel
的sentinel
会发现这个新的sentinel
- 新加入的
sentinel
和其他sentinel
节点建立长连接
故障发现
sentinel
节点定期向master
节点发送心跳包判断是否存活,一旦发现master
没有正确响应,sentinel
就会把master
设为主观不可用状态,然后把状态发送给其他sentinel
确认,当确认的sentinel
超过quorum
时,就认为master
节点客观不可用,接着就进入选举新的master
的流程,这里使用到了Raft
算法,基于投票的算法,只要保证过半数节点通过提议即可。
哨兵的主要功能
- 集群监控 哨兵可以监控master节点和slave节点的运行情况
- 消息通知 当集群中有某一个节点挂了之后,可以通知管理员
- 故障转移 当master节点挂了之后,哨兵会从slave节点中重新选举一个做为master节点
- 配置中心 如果发生故障转移,通知客户端新的master节点地址
Redis分片(Cluster)
主从同步的集群中,每个节点都存有集群中的所有数据,存在着单机存储量的瓶颈问题,形成了木桶效应。对Redis进行分片集群,可以提高Redis的性能和存储能力。
集群结构如上图,一个Redis Cluster由多个Redis节点构成,不同节点组服务没有交集,也就是每一个节点组对应数据sharding的一个分片。节点组内部分为主备两类节点,对应master和slave节点。两者数据准实时一直,通过异步化的主备复制机制来保证。一个节点组有且只有一个master节点,同时可以有0到n个slave节点,在这个节点组中,只有master节点可以提供读写服务,slave只能提供读服务。
集群的数据分片
Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念.
Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:
- 节点 A 包含 0 到 5500号哈希槽.
- 节点 B 包含5501 到 11000 号哈希槽.
- 节点 C 包含11001 到 16384号哈希槽.
这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.
如何把一次操作的所有值保存到一个节点中
Redis中引入了HashTag的概念,可以是得数据分布算法可以根据key的某一个部分进行计算,
举个简单的例子,加入对于用户的信息进行存储, user:user1:id、user:user1:name/ 那么通过hashtag的方式, user:{user1}:id、user:{user1}.name; 表示
当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash。
扩容
- 槽位迁移
- 数据迁移
Redis缓存问题(缓存击穿、缓存穿透、缓存雪崩、缓存不一致)
缓存击穿
当Redis缓存中的某个热点key失效,导致大量的请求穿过缓存,到达DB
缓存穿透
大量的无效请求到达数据库,比如请求参数id = -1,redis和数据库中都没有数据,但是会一直查询数据库。
解决办法
- 热点数据设置永不失效
- 多级缓存
- 熔断降级
- nginx拉黑多次请求的同一ip
- 参数校验,永远不要相信用户
- 当查询到数据库中没有的值,可以设置一个为null或其他提示的数据到缓存中,下次请求就不会到数据库中了,缓存的失效时间设置短一点如30秒
- 使用互斥锁
- 布隆过滤器
缓存雪崩
同一时间Redis缓存的key大面积失效,导致所有的请求都到达了数据库
- 一般这种情况是redis的过期时间到了,可以设置一个随机值,让key在一定范围内失效
- Redis分片,将key保存到不同的节点上,防止出现大面积失效的情况
缓存更新方式
这是决定在使用缓存时就该考虑的问题。
缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。
当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。
这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。
但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式。
数据不一致
第二个问题是数据不一致的问题,可以说只要使用缓存,就要考虑如何面对这个问题。缓存不一致产生的原因一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;或者是异步更新失败导致。
解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。