Java后端面经(八股——Redis)
Redis
定义:
Redis是一个Key-Value基于内存的数据库
适合存储数据种类:
(1)热点数据、需要频繁访问(热门商品信息、实时排行榜):访问内存的速度远远快于
磁盘。
(2)临时数据、易失数据(验证码、限流计数器):redis支持自动过期。
(3)需要原子操作的数据(执行Lua脚本过程中不能被其他命令打断):redis为单线程模型。
启动:
应当使用cmd命令行启动并指定其配置文件,否则会使用其内置配置。
数据持久化:
由于基于内存所以当Redis服务由于故障异常中断时,redis中的数据会丢失,但redis提供两种方式持久化,RDB和AOF
RDB:
会将当前内存中存储快照转化为磁盘文件,其在配置文件中配置,如save 60 1表示当60秒进行一次数据修改操作时会存储一次快照,即维护一个计时器(其每次衰减时间由配置中的hz控制默认为0.1s),当计时器结束时,如果这段时间中发生了1次修改则会生成一个RDB文件,其中使用shutdown默认参数为shundown save即立刻生成一次快照
AOF:
记录的是操作记录而非数据,其配置有always、everysec、no,分别对应每次执行操作都写入磁盘(主进程IO),每秒将该秒内操作写入磁盘(后台IO),由操作系统决定何时写入磁盘(后台IO)。
需要注意RDB的操作为Redis的子进程执行,Redis运行时会fork一个子进程,完全复制主进程的内存数据,当需要存储快照时由子进程进行IO操作;AOF则由主进程在执行完命令后再将命令写入缓冲池,根据配置决定主或后台进程进行IO;
当同时启用RDB和AOF时优先使用AOF恢复数据。
其中RDB对性能影响更小,但恢复时更容易丢失文件,AOF则由于主线程需要写命令到缓冲池开销更大,但设置always时能够保证每条命令都被记录,恢复时不会丢失数据,但aof文件更大
支持的存储类型:
del key 删除键,所有类型
String :
set key value
get key
HashMap:
hset key field value 向一个名称为Key的Map插入Entry<field,value>
hget key field 获取Key名称的Map的键为field的value
hdel key field 删除Key名称的Map的field对应的键值对
List:
lpop key 弹出并获取名称为key的list的左侧的值
lpush key value 向名称为key的list的左侧插入value
rpop key
rpush key value
lrange key start stop 获取名称为key的list指定范围的value
Set:
SADD key member:向集合添加一个或多个成员。
SREM key member:移除集合中的一个或多个成员。
SMEMBERS key:返回集合中的所有成员。
SISMEMBER key member:判断 member 是否是集合的成员。
SUNION key1 key2 ...:返回多个集合的并集。
Sorted Set, ZSet
ZADD key score member:向有序集合添加成员及其分数。
ZRANGE key start stop [WITHSCORES]:按排名返回指定区间的成员。
ZREM key member:移除有序集合中的一个或多个成员。
ZSCORE key member:返回成员的分数。
ZCOUNT key min max:计算指定分数区间内成员的数量
并发问题:
redis是单线程模型,即所有操作均由主线程按序操作,无需加锁,不存在多线程竞争和并发冲突的问题。
事务:
尽管单个增删改查不会出现并发问题,但如果设计涉及多个操作必须连续执行,不被其他客户端命令插入,需要事务。
事务的声明multi,声明后输入需要操作的指令,输入时不会影响其他客户端执行命令,命令编写完毕后,输入exec开始执行事务,在执行事务时会阻止其他客户端执行命令。discard撤掉事务的创建
watch key可以实现对key的乐观锁,在watch key命令执行瞬间记录其版本信息,当事务执行exec时发现当前watch的key值发送变化则不会再执行事务,返回nil。watch 主要用于保证在获取数据后,通过命令对数据进行修改输入的这段时间,key值不会变化。
redis本身不提供事务回滚功能,当执行事务出错时不会撤销已经执行过的命令。
Lua脚本具有原子性,可以将其看作一个事务,其中间出错时同样不会回滚。
分布式锁:
单节点:
SET key random_value NX PX timeout 其中NX表示当键不存在时才能操作成功,否则返回nil,PX则设置锁超时的时间,防止死锁,当服务操作时长可能超过设置时长时,应当使用看门狗机制,后台线程定期续期,看门狗线程应当是守护线程,当业务线程挂掉时会自动停止。random_value则确保不会删除服务的锁。其中释放锁的时候需要使用Lua脚本将验证random_value和删除锁作为一整个原子操作防止验证value后锁自动过期然后其他服务加锁后又被自己删除。
多节点:
使用相同的key和value,向所有节点发送加锁请求,如果超过一半的节点加锁成功且用时小于过期时长则加锁成功,否则向所有节点发送释放锁的请求
以及如果一个线程已经拿到锁,另外一个线程试图set时会返回nil而非阻塞等待,因此需要手动实现重试机制。
淘汰策略(可配置):
淘汰发生在当redis占用内存满时,有新的数据添加
- 默认报错
- 所有键最近最少lru
- 设置超时键最近最少
- 全部键随机
- 设置超时键随机
- 剩余超时时间最短
- 全部键最不常使用lfu
- 超时键最不常使用
lru原理:最近被访问的键未来更可能访问,淘汰最久没访问的键
lfu:淘汰访问频次更少的键
过期键删除策略(可配置):
- 惰性删除:过期时不直接删除,当再次使用这个键时检查是否过期,过期则删除掉
- 定期删除:每隔固定时间随机抽取部分键检查,过期则删除
Pipline
传统redis需要发一条指令->redis执行并返回->再下一条命令
使用pipline能够将多条命令打包一起发送,redis按顺序执行指令并返回所有的结果
增加了网络吞吐量,减少了等待时间
注意这些命令需要没有因果依赖
数据库与缓存数据一致性:
缓存和数据库主流模式
读:先读缓存,未命中则需要再去读数据库,并更新缓存
写:修改数据库中数据,并直接使缓存中数据失效(使用失效而非更新原因在于一是可能后续不再访问该数据,二是两次并发更新可能出现缓存错误覆盖问题)
可能出现缓存击穿的问题,以及更新数据库和删除缓存间的时间窗口可能导致数据不一致。
(当慢读快写时,当一个数据在缓存中不存在,A去读并准备将旧数据更新到缓存,B去写并执行删除缓存操作尽管目前缓存中并没有对应数据,B删除完毕后A读完并将读取的数据放入缓存即导致缓存中和数据库中数据不一致,解决策略延迟双删,即B先删除一次等待一段时间后再次删除缓存)
缓存穿透/击穿/雪崩
缓存穿透:
频繁请求数据库不存在的数据导致每次都需要访问数据库
- 参数校验
接口参数校验,异常用户/IP限流
2.缓存空值
即使查询不到对应数据也缓存一个空对象,设置一个较短的过期时间(当数据库更新后需要等待一段时间才能同步到用户)
3.布隆过滤器
每次向数据库插入数据时先更新过滤器,访问数据库时先访问过滤器判断是否有数据
过滤器原理,对于请求数据的多个字段进行hash,并维护一个bitmap将map中对应hash的值设置1,可能将不存在的数据判为存在,即部分字段相同,但不会将不存在的判为存在。不能同步数据库的删除,一位为1可能是多条数据设置过。如果自己手动实现需要注意同步过滤器的记录到磁盘,或者不同步直接冷启动从数据库重建,可以使用redis提供的实现。
缓存击穿:
(强调一个热点key过期)
当热点key过期时大量的请求同时访问数据库
1.逻辑过期
设置数据永不过期,数据内部维护一个过期字段,线程拿到数据后判断是否过期,过期则开启另外一个线程,异步地拿取分布式锁并更新缓存,其他线程直接返回旧数据(数据一致性差,性能好)
2.分布式锁更新
当发现数据不存在时,线程去拿分布式锁,然后去更新缓存再释放锁,其他线程阻塞等待,锁释放后拿到锁的线程判断是否缓存中有数据(数据一致性强,性能差)
缓存雪崩:
(强调多个热点key过期)
大量缓存数据过期/不可用,或redis宕机导致大量请求访问数据库
- 过期添加随机值防止同时过期
- 对于不常变化的数据设置永不过期,更新数据时异步更新缓存
- 业务高峰前对缓存做缓存预热
- 对于不可控的因素,做主从,集群,异地容灾