# Redis面试总结
# Redis如何持久化数据?
Redis自身是一个内存字典服务,数据读写均发生在内存中,内存中数据断电后就会丢失,因此Redis需要一些机制做持久化,以便Redis重启后的数据恢复。Redis中有两种持久化方案,分别是RDB以及AOF。
# RDB快照
RDB指的是Redis Database Backup(这个名称叫什么毫无意义,只是Redis叫RDB)是一种快照技术,当Redis运行时,系统自身会不定期的创建快照文件,两次快照创建的中间时间段的Redis数据不会写入快照。
Redis快照生成有两种方式,分别是手动触发以及自动触发:
手动触发
执行save以及bgsave指令的时候,Redis会生成快照。自动触发
redis默认自动触发save m n 指令生成快照,此外在主从同步的时候会触发bgsave指令生成快照。
# AOF追加文件 默认持久化方式
AOF(Append Only File)日志追加文件是Redis另一种持久化方案,该文件完整的记录了Redis中所有的写操作。Redis中默认的持久化方式是AOF。
# Redis如何恢复持久化数据?
在Redis重启时,我们需要恢复磁盘中的数据到内存中,接下来以混合持久化方式进行说明。通过设置aof-use-rdb-preamble参数为yes,我们可以开启混合持久化模式(生产推荐设置该参数),redis在恢复数据时,会按顺序执行以下的恢复策略:
- 尝试获取AOF文件和RDB文件,如果可以获取到两个文件,使用DDB快照恢复数据,然后使用AOF增量恢复快照到当前AOF文件最新更新点的数据。
- 如果AOF或RDB文件仅一个存在,使用存在的一个文件恢复数据。
- 如果AOF以及RDB文件均不存在,创建一个新的Redis数据库。
# Redis过期键的删除策略是什么?
Redis本身支持对key设置过期时间,当达到过期时间后,Redis并不会立刻删除数据,只有在数据被访问的情况下,Redis才会判断该键是否过期,如过期,Redis会删除数据并返回nil。这个删除策略叫惰性删除策略。
显然惰性删除策略很可能导致Redis内存使用率高的问题,因此Redis还提供了定时删除策略与惰性删除策略结合使用。默认情况下,Redis每100ms执行一次定时任务检索16个库,每个库随机检索20个键,如果键过期则执行清理。
# Redis中的事务机制?
Redis中没有传统的SQL数据库的事务的ACID的概念,仅提供Redis自定义的事务,即将多个命令打包,作为一个隔离的、连续的操作单元来执行。通过MULTI ... EXEC指令可以保证队列中的所有命令会按顺序、不间断地执行。在执行过程中,不会被其他客户端的命令插入。这保证了在并发环境下,这一批命令看到的数据视图是连续的。但是如果某个命令执行失败,Redis会继续向下执行其他的命令,不会执行回滚操作。
虽然Redis自身不支持ACID的特性,但是从它的设计哲学角度来看,Redis中的MULTI ... EXEC操作在一般情况下满足事务的四个特性。下面分别进行说明:
原子性
Redis中每一条指令都是原子操作,在不考虑语法错误的情况下,每条命令都能保证执行成功,即不会执行失败,所以一个批处理操作在指令正确编写的前提下,同样能保证100%全部执行成功,也就是说虽然redis事务报错不回滚,但是redis事务中的指令本身也不会报错,所以可以认为redis事务满足原子性。隔离性
在Redis事务执行时,只有当前事务会被执行,其他操作全部会被阻塞,可以理解为“串行化”的隔离级别,因此,Redis事务也满足隔离性。持久性 Redis自身是内存数据库,本身也不需要过度强调持久化的概念,一般情况下操作执行结果会被写入内存,后续会交由快照RDB/AOF追加日志写入磁盘,同样也可以理解为Redis事务支持持久化。
一致性 与传统事务的ACID一致,只要事务满足原子性、隔离性、持久性,那么事务就一定满足一致性。因此Redis满足一致性要求,至此4个事务的特性都满足,可以认为Redis支持事务,只是这个事务支持有点不太“传统”。
# Redis主从数据同步原理?
Redis主从节点数据同步主要分三个阶段,第一阶段建立连接,第二阶段数据同步,第三阶段命令传播
# 建立连接
从节点和主节点之间建立Socket通信,这部分本身没有什么特殊的。
# 数据同步
从节点在数据同步过程中会缓存主节点的关键配置信息(运行ID、复制偏移量),并存储在从节点的配置文件中(持久化存储),如果从节点在数据同步发起时从配置中无法检索到运行ID,则本次同步是全量同步,如果能检索到运行ID,则本次同步是增量同步。
# 命令传播
主节点中新的写入命令通过异步方式同步给从节点,从节点执行命令完成数据的同步。
# Redis的Java客户端
| 客户端 | 特点 |
|---|---|
| Jedis | 以redis命令作为方法名称,学习成本低,简单易用。但是缺点是非线程安全,多线程情况需要配合Jedis自己的连接池使用。 |
| Lettuce | 基于Netty实现,支持同步,异步以及响应式编程,线程安全。支持哨兵模式,集群模式,管道模式。 |
| Redisson | 基于Redis实现的分布式,可伸缩的Java数据结构集合。 |
| Spring Data Redis | Spring Data模块基于Jedis以及Redisson做了适配,推荐使用(实际项目中用的就是这个)。 |
# Redis主从+Sentinel实现原理
Redis有两种高可用方案,一种是cluster分片集群,另外一种是主从节点+Sentinel哨兵实现,因为实际项目中使用的是哨兵方案,因此这里针对该方案做说明。
# 原理简述
# 主从节点
Redis主从+哨兵的方案分两部分,一部分是主从节点,另外一部分是哨兵,不管用不用哨兵,Redis主从节点都是固定的,一般是一主两从,主节点做读写,从节点仅做读取操作。如果主节点宕机则主从集群不可用。
# 哨兵
Redis提供了Sentinal机制来实现主从集群的自动故障恢复。哨兵的作用如下:
- 监控
Sentinal会通过心跳机制不断检测master和slave是否按预期工作。 - 自动故障恢复
当Redis集群中master宕机后,Sentinal会选举一个slave为master。故障实例恢复后会作为slave加入集群。 - 通知
Sentinal作为Redis客户端的“服务发现”来源,当集群发生故障转移的时候,Sentinal会将最新的集群信息推送给Redis客户端。
# 哨兵具体如何实现故障转移
在Redis集群运转正常的情况下,哨兵节点仅监控Redis运行,此时哨兵集群(假定3节点)节点与节点之间完全独立,没有主从概念,一旦某哨兵节点感知到Redis主节点宕机,哨兵节点之间就会使用Paxos共识算法选举出一个主节点(注意是哨兵的主节点,不是Redis的主节点),由主节点负责整个故障转移的具体实施工作(故障转移的底层实现原理是手动修改Redis配置文件,以实现Redis主从的切换)。故障转移完成后,哨兵的主从关系同样会断开,此时哨兵节点之间完全独立,不具备主从的概念。
# Redis分布式锁实现原理
Redis分布式锁其实就是Redis里一条key-value键值对数据,这条键值对数据在一定时间后失效。
# 加锁操作
# 设置 key-value NX:仅在不存在时创建 EX设置过期时间,设置为30s
# 这条指令是单一指令,满足原子操作
SET lock_key unique_value NX EX 30
# 解锁操作
-- 解锁操作要先检查key是否存在,如果存在则删除key 这两个操作加一起不是原子操作,因此需要通过lua脚本实现
-- redis自身定义了lua脚本是原子操作执行(类似Redis事务的概念,也是Redis自身保证原子操作)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
# Java代码实现分布式锁
@Component
public class RedisDistributedLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 尝试获取分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识(唯一值)
* @param expireTime 过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
try {
RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
RedisConnection connection = connectionFactory.getConnection();
// 使用SET NX EX命令原子性地加锁
String result = connection.set(
lockKey.getBytes(StandardCharsets.UTF_8),
requestId.getBytes(StandardCharsets.UTF_8),
Expiration.seconds(expireTime),
RedisStringCommands.SetOption.SET_IF_ABSENT
);
connection.close();
return "OK".equals(result);
} catch (Exception e) {
log.error("获取分布式锁异常", e);
return false;
}
}
/**
* 释放分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String requestId) {
// 使用Lua脚本保证原子性
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
try {
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
Long result = redisTemplate.execute(script, Arrays.asList(lockKey), requestId);
return result != null && result == 1;
} catch (Exception e) {
log.error("释放分布式锁异常", e);
return false;
}
}
}
补充说明-Mysql中分布式锁实现
- 分布式锁的实现方案不止Redis一种,像ZooKeeper,Mysql都可以拿来做分布式锁,且都有实际的应用场景。
实际项目中quartz本身就支持分布式实现(如集群有3个节点,只有一个节点会执行定时任务,底层就是基于Mysql的行锁来实现的)。
# Redis和Mysql如何保证数据一致性?
这个问题其实可以抽象为分布式事务,但是显然不需要使用分布式事务的方案保证一致性(过于重量级了)。可选的简单方案是在业务层面更新操作完成后,尝试删除Redis中的缓存/更新缓存即可。
# Redis单线程为什么那么快?
Redis 的“单线程”指的是 处理网络请求和执行命令的核心模块(命令解析、数据处理、发送响应)是单线程的。并不是整个 Redis 实例只有一个线程。像持久化、异步删除、集群数据同步等操作是由额外的后台线程处理的。
之所以单线程还能那么快,是因为底层实现使用的是操作系统提供的I/O多路复用技术epoll,且使用的是水平触发,这才是Redis快的根本原因,此外Redis不直接操作磁盘,而是操作内存,这是它快的另外一个原因。
补充说明-Nginx及边缘触发技术
Nginx同样也是使用I/O多路复用技术epoll,且使用的是边缘触发,性能相对Redis更快。