# Redis面试总结

# Redis如何持久化数据?

Redis自身是一个内存字典服务,数据读写均发生在内存中,内存中数据断电后就会丢失,因此Redis需要一些机制做持久化,以便Redis重启后的数据恢复。Redis中有两种持久化方案,分别是RDB以及AOF。

# RDB快照

RDB指的是Redis Database Backup(这个名称叫什么毫无意义,只是Redis叫RDB)是一种快照技术,当Redis运行时,系统自身会不定期的创建快照文件,两次快照创建的中间时间段的Redis数据不会写入快照。
Redis快照生成有两种方式,分别是手动触发以及自动触发:

  1. 手动触发
    执行save以及bgsave指令的时候,Redis会生成快照。

  2. 自动触发
    redis默认自动触发save m n 指令生成快照,此外在主从同步的时候会触发bgsave指令生成快照。

# AOF追加文件 默认持久化方式

AOF(Append Only File)日志追加文件是Redis另一种持久化方案,该文件完整的记录了Redis中所有的写操作。Redis中默认的持久化方式是AOF。

# Redis如何恢复持久化数据?

在Redis重启时,我们需要恢复磁盘中的数据到内存中,接下来以混合持久化方式进行说明。通过设置aof-use-rdb-preamble参数为yes,我们可以开启混合持久化模式(生产推荐设置该参数),redis在恢复数据时,会按顺序执行以下的恢复策略:

  1. 尝试获取AOF文件和RDB文件,如果可以获取到两个文件,使用DDB快照恢复数据,然后使用AOF增量恢复快照到当前AOF文件最新更新点的数据。
  2. 如果AOF或RDB文件仅一个存在,使用存在的一个文件恢复数据。
  3. 如果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操作在一般情况下满足事务的四个特性。下面分别进行说明:

  1. 原子性
    Redis中每一条指令都是原子操作,在不考虑语法错误的情况下,每条命令都能保证执行成功,即不会执行失败,所以一个批处理操作在指令正确编写的前提下,同样能保证100%全部执行成功,也就是说虽然redis事务报错不回滚,但是redis事务中的指令本身也不会报错,所以可以认为redis事务满足原子性。

  2. 隔离性
    在Redis事务执行时,只有当前事务会被执行,其他操作全部会被阻塞,可以理解为“串行化”的隔离级别,因此,Redis事务也满足隔离性。

  3. 持久性 Redis自身是内存数据库,本身也不需要过度强调持久化的概念,一般情况下操作执行结果会被写入内存,后续会交由快照RDB/AOF追加日志写入磁盘,同样也可以理解为Redis事务支持持久化。

  4. 一致性 与传统事务的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中分布式锁实现
  1. 分布式锁的实现方案不止Redis一种,像ZooKeeper,Mysql都可以拿来做分布式锁,且都有实际的应用场景。
    实际项目中quartz本身就支持分布式实现(如集群有3个节点,只有一个节点会执行定时任务,底层就是基于Mysql的行锁来实现的)。

# Redis和Mysql如何保证数据一致性?

这个问题其实可以抽象为分布式事务,但是显然不需要使用分布式事务的方案保证一致性(过于重量级了)。可选的简单方案是在业务层面更新操作完成后,尝试删除Redis中的缓存/更新缓存即可。

# Redis单线程为什么那么快?

Redis 的“单线程”指的是 处理网络请求和执行命令的核心模块(命令解析、数据处理、发送响应)是单线程的。并不是整个 Redis 实例只有一个线程。像持久化、异步删除、集群数据同步等操作是由额外的后台线程处理的。
之所以单线程还能那么快,是因为底层实现使用的是操作系统提供的I/O多路复用技术epoll,且使用的是水平触发,这才是Redis快的根本原因,此外Redis不直接操作磁盘,而是操作内存,这是它快的另外一个原因。

补充说明-Nginx及边缘触发技术

Nginx同样也是使用I/O多路复用技术epoll,且使用的是边缘触发,性能相对Redis更快。