1. Redis
底层原理:单线程+多路IO复用
2. 基本数据类型
2.1 String
2.2 list
2.3 set
2.4 zset
2.5 hash
3. 特殊数据类型
3.1 Geospatial
地理信息,添加经纬度之后可以计算两个地点之间的距离
3.2 Bitmaps
签到、记录用户每天是否活跃,比如记录一周考勤 value 可以是 0000000,如果周一到周五都打卡了就设置为1 ,value 变成 1111100
3.3 HyperLogLog
解决基数计算问题:求集合中不重复元素个数的问题称为基数问题
相当于可以实现去重,但是和 set 不同,HyperLogLog 无法返回集合中的元素有哪些,只能告诉你基数统计的结果。如果添加的元素已经存在就返回 0 否则返回 1,优势是需要花费的内存极小就可以完成计算
4. 发布订阅
# 一个客户端订阅 channel1
subscribe channel1
# 另一个客户端向 channel1 发生 helloworld
publish channel1 helloworld
5. redis 事务
redis 事务本质就是串行多个指令,开始执行就不受其他命令打断
# 开启事务 相当于 mysql 的 begin
multi
# 执行事务
exec
# 取消本次事务组队
discard
- 要执行 exec 命令事务中包含的所有指令才开始执行
- 组队阶段报错,所有操作会被取消 (比如一个指令是 set k1 没有指定value值)
- 执行阶段报错,就会出现部分成功,部分失败 (比如一个指令是 incr k1,但是 k1 的value 不是数值类型,就会出现执行阶段报错,组队阶段不报错)
- 执行不保证原子性,会出现部分指令执行成功,部分失败的情况。而且失败之后也不会回滚
redis 乐观锁实现
原理就是检查版本号
# 再开启事务之前执行 watch,监视某个key的值是否发生变化,如果发生了变化接下来事务的执行exec就会失败
watch key名称
redis 不能实现悲观锁
6. 持久化
6.1 RDB(Redis Database)
再一定的时间间隔后,将内存中的数据快照写入到磁盘中
通过再配置文件中设置 save 的规则,比如10秒内有3个key发生了变化就进行持久化操作。
开启和关闭 RDB
Redis默认开启的就是RDB模式
通过在配置文件 redis.conf 配置 save 例如 save 10 2
表示10秒内有 2个key 发生修改就进行持久化
如果不开启就将 save 注释,或者配置空字符串 save ""
备份指令
# 备份命令
# 会阻塞主进程,期间redis不能正常使用
save
# 不会阻塞主进程,开启fork 子进程
bgsave
bgsave 备份过程
开启一个 fork 进程,将当前 redis 的数据快照写入到一个临时文件中,然后再去替换原有的 dump.rdb 持久化文件,这个过程叫做“写时复制”。过程是再后台异步执行的,不阻塞redis的正常请求
备份恢复
只需要将备份生成的 dump.rdb 文件放到redis启动目录下,然后重启redis就会自动读取
优点:
- 适合大规模数据复制
- 速度较快
缺点:
- 因为需要写入一份临时文件中,所以对磁盘空间有一定的要求
- 如果没有达到设置的规则,比如 10 秒内只有一个key 发生了变化,此时不会进行持久化操作,如果发生宕机这一部分修改就丢失了
6.2 AOF (Append Only File)
AOF 和 RDB 只能开启一个
如果同时开启了 AOF 和 RDB ,就不会去读取 dump.rdb 中的数据了。因为 appendonly.aof 的数据认为是更完整的
将写入的操作追加保存到 appendonly.aof 文件中
开启方式
配置文件中设置 appendonly yes
刷盘频率
当进行写入操作时,会先记录到 aof buffer 中,然后再以一定的频率刷新到磁盘文件中,可以配置以下三种级别
appendfsync always:始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no:redis不主动进行同步,把同步时机交给操作系统。
备份恢复
只需要将备份生成的 appendonly.aof 文件放到redis启动目录下,然后重启redis就会自动读取
优点:
- 数据完整性更好,不容易丢失数据
缺点:
- 备份恢复速度慢
- 如果是每次写入都刷盘到文件中,会有一定的性能压力
- 文件占用空间更大
建议
RDB 和 AOF 两者都开启
7. 主从复制
从服务器第一次和主服务器建立连接时会发送 sync 同步指令,此时主服务器会进行一次 rdb 持久化操作。然后将 dump.rdb 文件传给从服务器,实现数据同步。之后就是主服务发生了写入操作再主动同步给从服务器
7.1 一主二仆
一个主节点两个从节点。
- 从节点只能读不能写
- 主机宕机,从机还是从机
- 主机重启还是主机
7.2 薪火相传
一个主节下以一个从节点,从节点下还有从节点
- 减轻主服务器的压力
- 从机可以作为其他从机的主机
- 如果 从1 宕机会导致 从2 无法同步数据
7.3 反客为主
主机宕机之后,从机可以成为主机
- 需要再从机上手动执行 slaveof no one
7.4 哨兵模式
启动一个哨兵监视主服务器,当主服务器宕机之后,选择一个从服务器成为主服务器,之前的主服务器重启之后自动成为从服务器
如果不使用哨兵模式,是无法自动产生新的主节点的。需要再从机上手动执行命令将从节点升级为主节点
最少需要3个哨兵,因为只有1个哨兵宕机了就完犊子了。一主二从起步就是 6 个节点
选举新的主服务器规则
依照如下步骤,如果一样就进行下一步
- 配置文件的
replica-priority
- 选择偏移量最小的,也就是同步主服务器数据最多的
- 选择 runid 最小的,redis启动时会生成
8. Redis 集群
无中心化集群配置
集群中的每一台服务器都可以作为集群的入门
例如我们按照业务部署了两台 Redis ,一台负责用户相关的数据记录,另一台负责商品相关数据的记录。我们想要存储的商品信息,可以是先发送到用户的 Redis 服务,然后再由用户 Redis 转发到商品 Redis。这就是无中心化
这样也实现了 Redis 的水平扩容。将不同业务的数据分散到不同的 Redis 服务器上。
8.1 slot
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽
每一个节点负责处理一部分插槽,例如下图
集群中的每一个节点都可以作为集群的入口。比如在节点A上,添加一个key,但是经过计算
这个 key 存储的插槽属于节点 B 的范围。这时会自动重定向到对应的节点上。
注意:
如果一条指令同时添加多个 key 需要保证这些key经过计算是属于同一个节点的插槽区域的,否则无法成功。当然也有解决办法就是在key后加上组id,比如 key1{group1}
9. 常见问题
9.1 缓存穿透
大量请求无法命中 Redis ,直接访问数据库
解决方案
- 对空值进行缓存,缺点就是存储大量空值的 key 浪费空间,也可能造成数据不一致,数据库中有值了但是缓存中确是空
- 使用布隆过滤器
- 设置可访问名单 bitmap
9.2 缓存击穿
某一个热点key过期的瞬间,大量请求进来,直接访问数据库
解决方案
- 临时调整过期时间
- 增加分布式锁
- 预先将热点数据写入缓存中,比如双11预先将一些热门秒杀商品加载到缓存中
9.3 缓存雪崩
Redis 中大量的key 同时失效,此时大量请求进来,直接访问数据库
也可能是 redis 宕机了
解决方案
- 设置过期时间的时候,增加一个随机的值,让key的过期时间产生差异
- 记录缓存过期的时间,提前进行一个提醒,再开启一个线程去增加过期时间
- 使用分布式锁或者队列
- 构建多级缓存
10. 分布式锁
假设我们部署了多个java服务,如果在java中加锁,那就是只有访问到这一台服务上的请求会被这个锁限制,其他服务是不知道已经上锁的。这时就需要分布式锁,Redis就可以实现分布式锁
# 加锁(最好设置过期时间,否则如果忘记释放锁就会造成问题)
setnx
# 释放锁(就是删除key)
del
一般流程就是先加锁,如果加锁失败就再一定时间之后再重试
10.1 误删锁问题
情况一:
由于设置了过期时间,就可能出现线程A拿到锁之后,中途可能出现了卡顿导致在锁的超时时间内没有执行完操作,锁自动过期了。这时线程B拿到了锁,开始进行操作。但是在线程B操作的过程中,线程A又恢复了正常,然后A执行完释放了锁。注意问题来了,A的锁早就过期了,他现在释放的是B的锁,就造成了问题
解决
设置key时,value可以设置一个 uuid。释放锁时,比较uuid是否一致,一致才能删除,保障了自己只能释放自己的锁
情况二
经过情况一的优化之后依然存在问题,在线程A比较完uuid一致之后,因为比较uuid和释放锁并不是原子操作,所以可能出现,比较完uuid之后,还没删除锁。过期时间就到了,自动释放,这时线程B拿到了锁准备执行,然后A删除了锁就把线程B的锁给误删了
解决
使用 lua 脚本,使得比较uuid和删除的操作变成原子操作,执行lua脚本时Redis不会执行其他的命令。这也就保证了即便比较完 uuid 之后,锁过期自动释放,也不会误删他人的锁,因为此时 lua 脚本没有执行完,其他人是无法进行加锁操作的