分布式

1. Spring Cloud 体系

核心思想

Spring Cloud 并非一个全新的框架,而是一套用于构建微服务架构的规范和解决方案的集合。它巧妙地利用了 Spring Boot 的自动配置和快速开发特性,将业界经过广泛验证的优秀微服务组件(如 Netflix OSS、Alibaba Nacos、HashiCorp Consul 等)进行封装和集成,为开发者提供了一站式的分布式系统开发工具箱,涵盖了服务治理、配置管理、熔断降级、智能路由、服务调用等方方面面。

Netflix OSS 常用组件(部分进入维护模式)
  • 服务注册与发现 (Service Discovery): Eureka
    • 作用: 提供一个服务注册中心。每个微服务启动时,将自己的网络地址等信息“注册”到 Eureka Server。其他服务(消费者)则从 Eureka Server “发现”并拉取所需服务提供者的地址列表,从而实现服务间的动态寻址和调用。
    • 使用: 服务端添加 spring-cloud-starter-netflix-eureka-server 依赖,并使用 @EnableEurekaServer 注解。客户端添加 spring-cloud-starter-netflix-eureka-client 依赖,并配置 Eureka Server 地址。
  • 服务调用与负载均衡 (RPC & Load Balancing): OpenFeign + Ribbon
    • 作用: Feign 让远程服务调用变得像调用本地方法一样简单。开发者只需定义一个接口,并使用 @FeignClient 注解,即可完成对远程服务的调用。Ribbon(现已被 Spring Cloud LoadBalancer 替代)则提供了客户端负载均衡能力,当从 Eureka 获取到多个服务实例地址时,Ribbon 会根据配置的策略(如轮询、随机)选择一个实例进行调用。
    • 使用: 添加 spring-cloud-starter-openfeign 依赖,在启动类上加 @EnableFeignClients,创建接口并使用 @FeignClient("service-name") 注解。
  • 熔断与降级 (Circuit Breaker): Hystrix
    • 作用: 当某个下游服务出现故障或响应缓慢时,为了防止故障在系统中蔓延(即“服务雪崩”),熔断器会快速失败,暂时切断对该服务的调用。同时,可以执行一个预定义的降级逻辑(Fallback),例如返回一个缓存的、默认的或友好的提示信息。
    • 状态: Hystrix 已进入维护模式,官方推荐使用 Resilience4j 或其他替代方案。
  • API 网关 (API Gateway): Zuul
    • 作用: 作为系统的统一入口,API 网关负责请求路由、协议转换、权限校验、流量控制、日志监控等。所有外部请求都先经过网关,再由网关分发到后端的各个微服务。
    • 状态: Zuul 1.x 已进入维护模式,官方推荐使用 Spring Cloud Gateway。

Spring Cloud Alibaba 详解

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,是 Spring Cloud 体系的重要实现。它集成了阿里巴巴开源的优秀组件,为开发者提供了更符合国内技术生态的选择。

  • 服务注册与发现 & 分布式配置中心: Nacos
    • 作用: Nacos (Naming and Configuration Service) 是一个功能丰富的平台,完美整合了服务注册发现配置管理两大核心功能。
      • 服务发现: 与 Eureka 类似,提供服务注册、发现和健康检查。但 Nacos 支持基于 DNS 和 RPC 的服务发现,并提供更实时的健康检查机制。
      • 配置管理: 可以作为分布式配置中心,对所有微服务的配置进行集中化管理。支持配置的热更新,即修改配置后无需重启服务即可生效。还支持配置的版本管理、灰度发布等高级功能。
    • 使用:
      1. 引入 spring-cloud-starter-alibaba-nacos-discoveryspring-cloud-starter-alibaba-nacos-config 依赖。
      2. bootstrap.properties (或 .yml) 文件中配置 Nacos 服务器地址和应用名。
      3. 使用 @Value@ConfigurationProperties 注解即可动态获取和刷新配置。
  • 熔断、降级与流量控制: Sentinel
    • 作用: Sentinel 是面向分布式服务架构的“流量的守护者”,以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。相较于 Hystrix,Sentinel 功能更强大,提供了可视化的监控和配置平台,并且支持热点参数限流等精细化控制。
    • 核心概念:
      • 资源 (Resource): Sentinel 中一切皆资源,可以是一个方法、一段代码或一个服务 URL。
      • 规则 (Rule): 定义如何保护资源,包括流控规则、降级规则、系统保护规则等。
    • 使用:
      1. 引入 spring-cloud-starter-alibaba-sentinel 依赖。
      2. 配置 Sentinel 控制台地址。
      3. 通过 @SentinelResource 注解来定义资源,并指定 Fallback (降级逻辑) 和 BlockHandler (流控/熔断逻辑)。
  • 分布式事务解决方案: Seata
    • 作用: Seata 是一个开源的分布式事务解决方案,提供了高性能且易于使用的分布式事务服务。它支持多种事务模式,包括 AT(自动补偿)、TCC、Saga 和 XA 模式,旨在解决微服务架构下的数据一致性问题。
    • 使用: 引入 spring-cloud-starter-alibaba-seata 依赖,配置 Seata Server 地址,并使用 @GlobalTransactional 注解开启全局事务。

2. 分布式ID:雪花算法(Snowflake)

原理

Snowflake 是 Twitter 开源的一种分布式 ID 生成算法,它能生成一个 64 位的 long 型数字作为全局唯一 ID。这个 64 位的 ID 由四部分构成:

  • 1位符号位: 最高位,固定为0,表示正数,无实际意义。
  • 41位时间戳 (Timestamp): 精确到毫秒级,是 (当前时间戳 - 起始时间戳) 的差值。41位可以表示 (241−1) 毫秒,大约可以使用 69 年。
  • 10位工作机器ID (Worker ID): 这 10 位可以被灵活划分,例如前 5 位代表数据中心 ID (Datacenter ID),后 5 位代表机器 ID (Machine ID)。这样总共可以支持 210=1024 台机器。
  • 12位序列号 (Sequence): 表示在同一毫秒内,同一台机器上生成的 ID 序列号。12位意味着每台机器每毫秒可以生成 212=4096 个不同的 ID。
优点
  • 全局唯一: 通过时间戳、机器 ID 和序列号的组合,可以保证在分布式环境下的 ID 唯一性。
  • 趋势递增: 由于时间戳在高位,所以生成的 ID 整体上是按时间趋势递增的,这对于数据库索引(特别是 B+树)非常友好,可以减少页分裂,提高插入性能。
  • 高性能: ID 在本地生成,不依赖任何外部服务(如数据库或 Redis),生成效率极高。
  • 高可用: 算法本身不依赖网络,部署简单,具有很高的可用性。
面试题:“雪花算法有时钟回拨问题,如何解决?”

回答要点:

时钟回拨是指服务器时间被同步到一个过去的时间点。如果算法不做处理,可能会生成重复的 ID。解决方案通常是:在生成 ID 时,记录上一次生成 ID 时所使用的时间戳。当发现当前时间戳小于上次记录的时间戳时,就意味着发生了时钟回拨。

  • 方案一(拒绝服务): 直接抛出异常,拒绝生成 ID,等待时钟恢复正常。这种方案简单,但会暂时影响可用性,适合对 ID 连续性要求不高的场景。
  • 方案二(等待追赶): 如果回拨幅度很小(比如几毫秒),程序可以 while(currentTime < lastTimestamp) 这样自旋等待,直到当前时间追赶上上次的时间戳。这会造成短暂的线程阻塞。
  • 方案三(使用备用位): 一些改进版的雪花算法会预留几位作为扩展位,当发生时钟回拨时,在这几位上做自增,从而在短时间回拨内仍能生成不同的 ID。(这种方案实现较为复杂)
  • 业界实践(美团 Leaf): 在发生时钟回拨时,切换到另一种备用 ID 生成策略(如号段模式),或者直接报错。

3. RBAC(基于角色的访问控制)

原理

RBAC (Role-Based Access Control) 是一种主流且灵活的权限管理模型。它的核心思想是在 用户 (User)权限 (Permission) 之间引入一个中间层——角色 (Role)。权限不再直接授予用户,而是授予角色;然后将角色分配给用户。这样,用户与权限实现了解耦,当需要修改大量用户的权限时,只需修改他们共同拥有的角色的权限即可,极大地简化了权限管理和维护。

核心三要素
  • 用户 (User): 系统操作的主体。
  • 角色 (Role): 权限的集合,代表了一组特定的职责或身份,如“管理员”、“文章编辑”、“普通会员”。
  • 权限 (Permission): 对系统中特定资源进行特定操作的许可。通常用一个字符串表示,如 user:createorder:deletearticle:publish
面试题:“请设计一个 RBAC 权限管理系统的数据库表结构。”

回答要点:

一个基础的 RBAC 模型至少需要五张表:

  • 用户表 (t_user): 存储用户信息。
    • user_id (主键), username, password, …
  • 角色表 (t_role): 存储角色信息。
    • role_id (主键), role_name (角色名, 如”管理员”), role_key (角色标识, 如”admin”), …
  • 权限表 (t_permission): 存储具体的权限点信息。
    • permission_id (主键), permission_name (权限名称, 如“新增用户”), permission_code (权限标识, 如 user:add), parent_id (用于菜单层级), …
  • 用户-角色关联表 (t_user_role): 存储用户和角色的多对多关系。
    • user_id (外键), role_id (外键)。(主键是 user_idrole_id 的联合主键)
  • 角色-权限关联表 (t_role_permission): 存储角色和权限的多对多关系。
    • role_id (外键), permission_id (外键)。(主键是 role_idpermission_id 的联合主键)

4. Redis 进阶详解

核心特点
  • 基于内存: Redis 是一个内存数据结构存储系统,所有数据都存放在内存中,因此读写速度极快。
  • 单线程模型: Redis 的核心网络模型处理客户端请求是单线程的。这避免了多线程环境下的上下文切换和锁竞争带来的开销。(注意:Redis 6.0 之后引入了多线程来处理 I/O,但执行命令的核心仍然是单线程)。
  • IO多路复用: 单线程能实现高性能的关键在于它使用了 IO 多路复用技术(如 Linux 下的 epoll)。该技术允许单个线程同时监听多个网络连接上的 IO 事件,当某个连接准备好读或写时,才去处理它,极大地提高了并发处理能力。
  • 丰富的数据类型: 支持 String, Hash, List, Set, Sorted Set, Bitmap, HyperLogLog, GEO 等多种数据结构。
缓存三大问题与解决方案
  • 缓存穿透:
    • 问题: 客户端查询一个数据库和缓存中不存在的数据。这导致每次请求都会绕过缓存,直接打到数据库上,当有大量此类请求时,会给数据库带来巨大压力。
    • 解决:
      1. 缓存空对象: 当从数据库查询不到数据时,也在缓存中存入一个特殊的空值(如 null 或特定字符串),并设置一个较短的过期时间。
      2. 布隆过滤器 (Bloom Filter): 在访问缓存之前,先通过布隆过滤器判断 key 是否可能存在。布隆过滤器可以高效地判断一个元素一定不存在,从而在第一层就拦截掉大量无效请求。
  • 缓存击穿:
    • 问题: 某个热点 Key 在某一时刻突然失效,而此时恰好有大量的并发请求访问这个 Key,这些请求会同时穿透缓存,直接打到数据库上,可能导致数据库瞬间崩溃。
    • 解决:
      1. 设置热点 Key 永不过期: 对于一些访问极其频繁且数据相对固定的热点数据,可以考虑不设置过期时间,通过后台任务定时更新。
      2. 使用分布式锁: 在查询数据库之前,先获取一个该 Key 对应的分布式锁。只有第一个获取到锁的线程才能去查询数据库并回写缓存,其他线程则等待或直接返回。
  • 缓存雪崩:
    • 问题: 大量的缓存 Key 在同一时间集中失效(例如,在应用启动时缓存了大量数据,并设置了相同的过期时间),导致所有请求都瞬间涌向数据库,造成数据库压力剧增甚至宕机。
    • 解决:
      1. 过期时间加随机值: 在设置缓存的过期时间时,在一个基础时间上增加一个随机数,使得 Key 的失效时间点尽可能分散。
      2. 多级缓存: 建立多级缓存体系,如 Nginx 缓存 + Redis 缓存 + JVM 本地缓存(Caffeine/Guava Cache)。
      3. 熔断降级: 使用 Hystrix 或 Sentinel 等组件,当检测到数据库压力过大时,进行熔断或降级处理,暂时不访问数据库,返回一个默认值或提示信息。

5. 消息队列(MQ)

核心作用
  • 异步 (Asynchronous): 将耗时的操作(如发送邮件、生成报表)作为消息放入 MQ,主流程可以立即返回,无需等待这些操作完成,从而提高系统的响应速度和吞吐量。
  • 解耦 (Decoupling): 生产者和消费者之间通过 MQ 进行通信,无需直接相互依赖。任何一方的修改、宕机或升级都不会影响到另一方,增强了系统的灵活性和可维护性。
  • 削峰 (Peak Shaving): 在秒杀、大促等高并发场景下,将瞬时涌入的大量请求暂存在 MQ 中,下游的消费者系统可以按照自己的处理能力,平稳地从 MQ 中拉取并处理请求,避免了流量洪峰直接冲垮下游服务。
面试题:“请列举你使用消息队列时可能遇到的问题,并给出解决方案。”

回答要点:

  • 消息丢失 (Message Loss):
    • 问题: 消息从生产者发出后,由于网络或 MQ 服务故障,未能成功到达消费者。
    • 解决:
      1. 生产者端: 开启生产者的 confirmack 机制,确保消息被 MQ 成功接收。如果发送失败,可以进行重试。
      2. MQ 服务端: 对消息进行持久化,防止 MQ 宕机导致消息丢失(如 RabbitMQ 的持久化队列和消息,Kafka 的磁盘存储)。
      3. 消费者端: 消费者在处理完业务逻辑后,再向 MQ 发送确认应答(ack)。如果处理过程中消费者宕机,MQ 没有收到 ack,会将该消息重新投递给其他消费者。
  • 重复消费 (Duplicate Consumption):
    • 问题: 由于网络抖动、消费者 ack 超时等原因,MQ 可能会重复投递同一条消息。
    • 解决: 核心是保证消费逻辑的幂等性 (Idempotence)。即多次执行同一个操作,结果都是相同的。实现方式有:
      1. 在数据库中为业务操作创建一个唯一键,每次操作前先检查该唯一键是否存在。
      2. 使用一个全局唯一的业务 ID(如订单号),在处理消息前,先查询这个 ID 是否已经被处理过(例如,存入 Redis Set 或数据库)。
  • 消息堆积 (Message Backlog):
    • 问题: 生产者的生产速度远大于消费者的消费速度,导致大量消息在 MQ 中积压,占用资源并可能导致消息超时丢失。
    • 解决:
      1. 水平扩展消费者: 增加消费者实例的数量,并行处理消息。这是最常用的方法。
      2. 优化消费逻辑: 检查消费者代码,看是否有可以优化的慢操作(如 I/O 密集型操作)。
      3. 消息分片/分区: 对 Topic 进行分区(Partitioning),让不同的消费者组处理不同的分区,提高并发度。
      4. 增加预警监控: 对消息堆积数量设置阈值,达到阈值时及时告警,人工介入。

6. 分布式锁详解

作用

在分布式系统环境下,多个进程或服务器上的多个线程需要访问同一个共享资源时,为了保证数据的一致性和操作的原子性,需要一种机制来确保在同一时刻,只有一个客户端能够持有锁并访问该资源。

实现方案对比
实现方式 优点 缺点 适用场景
基于数据库 实现简单,直接利用数据库(如唯一索引、悲观锁 for update)。 性能开销大,有锁库锁表的风险,不可重入,不是阻塞锁,数据库单点故障问题。 并发度不高的简单场景。
基于 ZooKeeper 可靠性高,天然支持阻塞锁和可重入,解决死锁问题(临时节点),无锁过期问题,支持公平锁。 性能不如 Redis,实现复杂,依赖 ZK 集群。 对可靠性要求极高,且能容忍一定性能损耗的场景,如分布式协调。
基于 Redis 性能极高,实现相对简单,有成熟的框架 (Redisson) 可用。 可靠性相对 ZK 稍差,需要处理锁过期和业务未执行完的问题,非公平锁。 互联网高并发、对性能要求高的绝大多数场景。
基于 Redis 的实现进阶
  • 加锁的正确姿势:

    使用 SET key value NX EX time 命令。

    • SET key value: 设置键值。value 通常是一个唯一的随机字符串(如 UUID),用于标识锁的持有者。

    • NX: (if Not eXists),确保只有在 key 不存在时才能设置成功,实现了“加锁”的原子性。

    • EX time: 设置一个自动过期时间(秒),防止因客户端宕机而导致死锁。

      这三个参数必须在一个命令中执行,保证原子性。

  • 解锁的正确姿势:Lua 脚本

    • 为什么需要 Lua: 解锁操作包含“判断”和“删除”两步:1. GET 锁的 value,判断是否与自己加锁时设置的随机字符串相等。2. 如果相等,则 DEL 锁。如果不用 Lua 脚本,在执行完第一步后,若该线程阻塞,此时锁恰好过期,另一个线程获取了锁,那么当原线程恢复执行第二步时,就会误删掉新线程的锁

    • Lua 脚本示例:

      Lua

      1
      2
      3
      4
      5
      6
      -- 脚本接收两个参数:KEYS[1] 是锁的 key,ARGV[1] 是加锁时设置的唯一值
      if redis.call('get', KEYS[1]) == ARGV[1] then
      return redis.call('del', KEYS[1])
      else
      return 0
      end

    Lua 脚本可以确保多个命令在 Redis 服务端被原子性地执行,杜绝了上述问题。

  • Redis 乐观锁:WATCH 命令

    • 作用: WATCH 命令可以监视一个或多个 key,如果在事务 EXEC 执行之前,任何一个被监视的 key 被其他命令修改了,那么整个事务将被取消,EXEC 返回 nil

    • 原理: 这是一种检查后设置 (Check-And-Set, CAS) 的实现。它不是真正的加锁,而是在更新数据时检查数据是否被修改过。

    • 使用场景: 适用于读多写少的并发场景,可以减少锁的开销。例如,更新商品库存。

      1. WATCH stock_key // 监视库存

      2. current_stock = GET stock_key // 获取当前库存

      3. (在客户端代码中判断 current_stock 是否足够)

      4. MULTI // 开启事务

      5. SET stock_key new_stock // 准备更新库存

      6. EXEC // 执行事务

        如果从 WATCH 到 EXEC 之间 stock_key 被其他客户端修改,EXEC 将失败,此时客户端需要重试整个操作。

面试题:“Redis 分布式锁锁过期了但业务没执行完怎么办?”

回答要点:

这是分布式锁的一个经典问题,被称为锁的超时续期问题。

  • 问题根源: 我们给锁设置了一个过期时间,比如 30 秒,但业务执行了 35 秒。在第 30 秒时锁会自动释放,其他线程就能获取到锁,导致并发问题。
  • 解决方案:“看门狗”(Watchdog)机制
    • 原理: 比如 Java 中的 Redisson 框架就内置了看门狗。当一个线程获取锁成功后,Redisson 会启动一个后台线程(看门狗),定期(例如每 10 秒)检查该线程是否还持有锁。如果持有,并且业务仍在执行,看门狗就会自动为这个锁延长过期时间(续期),比如再续 30 秒。这个过程会一直持续,直到业务执行完毕,线程主动释放锁,看门狗才会停止。
    • 总结: 看门狗机制通过后台线程自动续期,确保了在业务执行完成之前,锁不会因为超时而提前释放,从而保证了锁的可靠性。

7. 分布式事务详解

作用

在微服务架构中,一个业务操作可能需要调用多个独立的服务来共同完成(例如,电商下单操作需要调用订单服务、库存服务、积分服务)。分布式事务旨在保证这些跨服务的数据库操作能够遵循 ACID 原则,要么全部成功,要么全部回滚,以确保数据的最终一致性

解决方案深入分析
  • XA (2PC/3PC): 两阶段/三阶段提交协议。
    • 角色: 事务管理器 (Transaction Manager, TM) 和 资源管理器 (Resource Manager, RM)。
    • 流程 (2PC):
      1. 准备阶段 (Prepare): TM 通知所有 RM 准备提交,RM 执行本地事务并锁定资源,但不提交,然后向 TM 报告准备就绪。
      2. 提交/回滚阶段 (Commit/Rollback): 如果所有 RM 都准备就绪,TM 通知所有 RM 提交;否则,通知所有 RM 回滚。
    • 评价: 是一种强一致性的方案,但协议复杂,性能差,同步阻塞模型会长时间锁定资源,且协调器存在单点故障风险,在互联网高并发场景下很少使用
  • TCC (Try-Confirm-Cancel): 补偿型事务。
    • 核心: 是一种业务层面的柔性事务方案,对代码侵入性强。
    • 流程:
      1. Try: 对业务资源进行检查和预留。例如,扣减库存操作,Try 阶段是冻结库存。
      2. Confirm: 如果所有服务的 Try 阶段都成功,则执行所有服务的 Confirm 操作,真正完成业务。例如,将冻结的库存真实扣减。
      3. Cancel: 如果任何一个服务的 Try 阶段失败,则执行所有已成功服务的 Cancel 操作,释放预留的资源。例如,解冻之前被冻结的库存。
    • 评价: 性能较好,数据一致性高于可靠消息方案。但开发成本极高,需要为每个操作都编写 Try, Confirm, Cancel 三个幂等的方法。
  • Saga 模式: 长事务解决方案。
    • 核心: 将一个大的分布式事务拆分成多个本地事务,每个本地事务都有一个对应的补偿操作
    • 流程:
      • 正向执行: Saga 协调器按顺序调用 T1, T2, T3…Tn。
      • 反向补偿: 如果 Ti 失败,Saga 会按相反顺序调用前面已成功事务的补偿操作 C(i-1)…C2, C1,进行回滚。
    • 与 TCC 对比:
      • TCC 有资源预留阶段,锁资源时间长;Saga 没有预留,直接提交本地事务,锁资源时间短。
      • TCC 的补偿是逆向操作 (Cancel);Saga 的补偿是反向操作
    • 评价: 适合于业务流程长、需要保证最终一致性的场景。同样对代码有侵入性,需要设计好每个子事务的补偿逻辑。
  • 基于可靠消息的最终一致性 (常用):
    • 核心: 这是微服务架构中最常用的柔性事务方案。
    • 原理: 服务 A 在执行完本地事务后,发送一条消息到 MQ。服务 B 订阅该消息,消费消息并执行自己的本地事务。
    • 关键问题: 如何保证本地事务执行消息发送的原子性?
      • 事务消息 (RocketMQ 支持): 生产者先发送一条“半消息”到 MQ,MQ 收到后不投递。然后生产者执行本地事务。如果事务成功,则向 MQ 发送确认,MQ 投递该消息;如果事务失败,则通知 MQ 删除该半消息。
      • 本地消息表: 业务操作和“待发送消息”记录在同一本地事务中。一个后台任务定时扫描这张表,将消息发送到 MQ,发送成功后更新表状态。
    • 评价: 实现了服务间的解耦,性能高,吞吐量大。但它不保证数据的强一致性,只保证最终一致性,存在一个短暂的数据不一致状态窗口。需要处理好消息的可靠投递和幂等消费问题。

Lua 脚本详解 (在 Redis 中的应用)

1. Lua 是什么?

Lua 是一种轻量级、可扩展的脚本语言,被设计用于嵌入到其他应用程序中,从而为应用程序提供灵活的扩展和定制功能。它以其简洁的语法、高效的执行性能和极小的内存占用而闻名。

在 Redis 的上下文中,Lua 脚本提供了一种在 Redis 服务器端执行复杂逻辑的强大能力。

2. 为什么 Redis 要支持 Lua 脚本?

  • 原子性 (Atomicity): 这是在 Redis 中使用 Lua 最核心的原因。Redis 会将整个 Lua 脚本作为一个单独的命令来执行,在脚本执行期间,不会有其他客户端的命令被插入执行。这完美地解决了需要组合多个 Redis 命令才能完成一个业务逻辑时,可能出现的竞态条件问题。例如前面提到的“判断锁并删除锁”的操作,如果分两步执行,就不是原子的,而封装在 Lua 脚本中就是原子的。
  • 减少网络开销: 对于需要多次与 Redis 交互的复杂操作,可以将所有逻辑封装在一个 Lua 脚本中,一次性发送给 Redis 服务器。客户端只需发送一次请求,而不是多次,这显著减少了客户端与服务器之间的网络往返时间(RTT),提升了性能。
  • 代码复用: 编写好的 Lua 脚本可以被缓存(通过 SCRIPT LOAD 命令生成一个 SHA1 校验和),之后客户端可以通过这个简短的 SHA1 校验和(使用 EVALSHA 命令)来调用脚本,避免了每次都发送完整的脚本内容。

3. 如何在 Redis 中使用 Lua 脚本?

通过 EVAL 或 EVALSHA 命令来执行。

EVAL script numkeys key [key …] arg [arg …]

  • script: 要执行的 Lua 脚本字符串。
  • numkeys: 后面跟的 key 参数的数量。这有助于 Redis 正确地将参数区分为键名(KEYS)和普通参数(ARGV),这对于 Redis Cluster 模式下的路由至关重要。
  • key [key ...]:脚本中要操作的 Redis 键,在 Lua 脚本中可以通过全局变量 KEYS table(例如 KEYS[1])来访问。
  • arg [arg ...]:传递给脚本的额外参数,在 Lua 脚本中可以通过全局变量 ARGV table(例如 ARGV[1])来访问。

示例:实现一个安全的库存扣减

Lua

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 脚本逻辑:检查库存是否充足,如果充足则扣减并返回1,否则返回0
-- KEYS[1]: 库存的 key,例如 "product:1001:stock"
-- ARGV[1]: 本次要扣减的数量

local stock = tonumber(redis.call('get', KEYS[1]))
local quantity = tonumber(ARGV[1])

if stock >= quantity then
redis.call('decrby', KEYS[1], quantity)
return 1
else
return 0
end

这个脚本保证了“读取库存”和“扣减库存”两个操作的原子性,避免了在高并发下超卖的问题。


Token 认证机制详解

1. Token 是什么?

Token(令牌)是在服务端生成的一串加密字符串,作为客户端进行请求的一个“凭证”。当用户第一次登录成功后,服务端会生成一个 Token 并返回给客户端。之后,客户端在每次请求需要身份认证的接口时,都需要在请求头(通常是 Authorization 字段)中携带这个 Token。服务端接收到请求后,会验证 Token 的有效性,如果验证通过,则处理该请求;否则,拒绝该请求。

一个典型的 Token 是 JWT (JSON Web Token),它由三部分组成,用 . 分隔:

  • Header (头部): 包含了令牌的类型(typ,即 JWT)和所使用的签名算法(alg,如 HMAC SHA256 或 RSA)。
  • Payload (负载): 包含了“声明 (claims)”,是存放实际需要传递的数据的地方。例如用户ID(sub)、签发时间(iat)、过期时间(exp)以及其他自定义的用户信息。注意:Payload 部分是 Base64 编码的,并非加密,因此不应存放敏感信息。
  • Signature (签名): 对前两部分(Header 和 Payload)使用指定的算法和存储在服务端的密钥(secret)进行签名。这个签名的作用是防止数据被篡改。服务端收到 Token 后,会用同样的算法和密钥重新计算签名,并与 Token 中的签名进行比对,若一致,则说明 Token 未被篡改且是可信的。

2. Token 认证原理(工作流程)

  1. 登录: 用户使用用户名和密码发起登录请求。
  2. 验证: 服务端验证用户的凭据是否正确。
  3. 签发 Token: 验证成功后,服务端根据用户ID、角色等信息,结合密钥(secret),生成一个 Token。
  4. 返回 Token: 服务端将生成的 Token 返回给客户端。
  5. 存储 Token: 客户端(浏览器、App)将 Token 存储起来,通常放在 localStoragesessionStorageHttpOnly 的 Cookie 中。
  6. 携带 Token 请求: 客户端在后续每次请求受保护的 API 时,都会在 HTTP 请求头的 Authorization 字段中附上 Token,格式通常为 Bearer <token>
  7. 验证 Token: 服务端收到请求后,从请求头中解析出 Token,然后:
    • 验证签名是否正确,确保 Token 未被篡改。
    • 检查 Token 是否在有效期内(exp 声明)。
    • 如果验证通过,则从 Payload 中获取用户信息,执行业务逻辑并返回结果。
    • 如果验证失败,则返回 401 Unauthorized 错误。

3. 为什么使用 Token?(与 Session 的区别)

在 Web 开发早期,Session-Cookie 机制是主流。服务端在用户登录后创建一个 Session 对象存储在内存或 Redis 中,并生成一个 Session ID,通过 Cookie 返回给浏览器。浏览器后续请求会自动带上这个 Session ID,服务端根据 ID 找到对应的 Session 信息来识别用户。

Token 机制相比 Session 机制,核心优势在于“无状态性 (Statelessness)”,这带来了以下好处:

特性对比 Session 机制 Token 机制 优势说明
状态存储 有状态 (Stateful)。Session 信息需存储在服务端。 无状态 (Stateless)。用户信息包含在 Token 的 Payload 中,服务端无需存储。 减轻服务端压力。服务端不需要为每个在线用户维护一个 Session 对象。
可扩展性 。在分布式或集群环境下,需要解决 Session 共享问题(如 Session Sticky、Session Replication、集中存储)。 。由于服务端不存储状态,任何一台服务器只要有相同的密钥,就能验证 Token,天然适合分布式和微服务架构。 轻松实现水平扩展。增加服务器节点无需额外配置 Session 共享。
跨域支持 有限。基于 Cookie 的 Session 机制在跨域(CORS)场景下处理起来比较麻烦。 优秀。Token 可以通过 HTTP 请求头发送,不受同源策略限制,非常适合前后端分离和跨域 API 调用。 适应现代架构。完美支持 SPA(单页应用)、移动 App 等多种客户端。
安全性 依赖 Cookie 机制,可能遭受 CSRF 攻击。 如果 Token 存储在 localStorage,可能遭受 XSS 攻击。需要综合考虑存储方式。 两者各有安全风险点,需配合其他安全策略。Token 机制不依赖 Cookie,更灵活。
适用性 适合传统的、一体化的 Web 应用。 适合现代的、分布式的、跨终端的(Web, Mobile, IoT)应用架构。 Token 更具通用性和前瞻性。

4. 双令牌策略 (Access Token + Refresh Token)

  • Q: 为什么不用单个 Token?
    • 如果 Token 有效期很长(如一个月): 安全风险高。一旦 Token 在此期间被窃取,攻击者可以长时间冒充用户身份进行操作。
    • 如果 Token 有效期很短(如 15 分钟): 用户体验差。用户需要频繁地重新登录,这是无法接受的。
  • A: 双令牌策略应运而生,完美平衡了安全性和用户体验。
    • Access Token (访问令牌): 它的有效期非常短(如 15 分钟到 1 小时)。它被用于访问受保护的资源,由于其生命周期短,即使被窃取,攻击者能造成的危害也有限。
    • Refresh Token (刷新令牌): 它的有效期很长(如 7 天或 30 天)。它的唯一作用是用来获取新的 Access Token。Refresh Token 本身不包含任何权限信息,不能用于直接访问 API。
  • 双令牌工作流程(静默刷新)
    1. 首次登录: 用户登录成功,服务端返回一个短期的 Access Token 和一个长期的 Refresh Token。客户端将两者都存储起来。
    2. 正常访问: 客户端使用 Access Token 访问 API。服务端验证 Access Token 通过,返回数据。
    3. Access Token 过期: 客户端再次使用过期的 Access Token 访问 API,服务端返回 401 Unauthorized 错误,并可能带上一个特定错误码,告知客户端是“令牌过期”而非“无效令牌”。
    4. 静默刷新: 客户端的请求拦截器捕获到这个 401 错误后,不会立即跳转到登录页。而是自动发起一个特殊的请求,携带那个长期的 Refresh Token 去访问一个专门的刷新接口(如 /api/token/refresh)。
    5. 签发新令牌: 服务端验证 Refresh Token 的有效性(通常会将其存储在 Redis 或数据库中进行比对,以实现强制下线功能)。如果验证通过,就生成一个新的 Access Token(有时也会生成一个新的 Refresh Token,这被称为刷新令牌滚动策略)并返回给客户端。
    6. 重试请求: 客户端收到新的 Access Token 后,用它替换掉本地旧的 Access Token,然后自动重新发送刚才因令牌过期而失败的那个请求
    7. 无感体验: 整个过程对用户是透明的,用户感觉不到令牌已经过期并被刷新,实现了“静默刷新”,体验非常流畅。
    8. Refresh Token 过期: 如果 Refresh Token 也过期了,那么刷新接口会返回错误,此时客户端才会真正清除用户凭证并引导用户重新登录。

5. Token 相关场景与面试题

  • 面试题 1:“Token 应该存储在哪里?localStorage、sessionStorage 还是 Cookie?”
    • 回答要点:
      • localStorage/sessionStorage:
        • 优点: 方便 JavaScript 直接读写,容量较大(5MB)。
        • 缺点: 容易受到 XSS (跨站脚本攻击)。如果网站存在 XSS 漏洞,攻击者可以执行 JS 代码直接窃取存储在其中的 Token。
      • Cookie (HttpOnly):
        • 优点: 设置为 HttpOnly 后,JavaScript 将无法读写该 Cookie,可以有效防御 XSS 攻击。浏览器会自动在同域请求中携带它。
        • 缺点: 容易受到 CSRF (跨站请求伪造) 攻击。攻击者可以诱导用户点击恶意链接,浏览器会自动带上用户的 Cookie 去请求你的网站,执行非用户本意的操作。需要配合 Anti-CSRF Token 等机制来防御。容量较小(4KB)。
      • 最佳实践/结论: 没有绝对完美的选择,需要权衡。
        • 高安全性方案: 将 Refresh Token 存储在 HttpOnly 的 Cookie 中(防止 XSS),将 Access Token 存储在内存中(变量里,页面刷新丢失)或 sessionStorage 中。同时,后端接口必须实施 CSRF 防御策略。
        • 主流实践方案 (前后端分离): 将 Token 存储在 localStorage 中,并在 Authorization 请求头中携带。同时,前端必须严格做好输入过滤和内容转义,尽最大努力防止 XSS 漏洞的出现。
  • 面试题 2:“用户点击“退出登录”时,Token 如何失效?”
    • 回答要点:
      • 对于无状态的 JWT: 由于所有信息都在 Token 自身,服务端无法主动让其失效。因此,“退出登录”主要是一个客户端行为
        • 客户端: 只需从 localStorage 或其他存储位置清除 Token 即可。用户将无法再发起认证请求。
      • 如何实现服务端强制下线?: 如果需要实现“踢人下线”或“修改密码后所有设备强制下线”的功能,就必须打破纯粹的无状态。
        • 黑名单机制: 服务端可以建立一个 Token 黑名单(例如,使用 Redis Set)。当用户退出登录时,将该 Token 的 jti (JWT ID) 或整个 Token 放入黑名单,并设置与 Token 剩余有效期相同的过期时间。在每次验证 Token 时,除了常规验证,还需检查该 Token 是否在黑名单中。
        • 基于 Refresh Token: 在双令牌模式下,退出登录时只需让服务端的 Refresh Token 失效(例如,从 Redis 中删除)。这样用户就无法再获取新的 Access Token,当旧的 Access Token 过期后,自然就下线了。
  • 面试题 3:“请你设计一个支持 Web 端和 App 端统一登录的认证系统。”
    • 回答要点:
      • 这正是 Token 认证机制的典型应用场景。我会采用基于 OAuth 2.0/OIDC 或自定义的**双令牌(Access/Refresh Token)**方案。
      • 统一认证中心 (UAC): 建立一个独立的认证服务,负责处理所有客户端(Web, iOS, Android)的登录、注册、Token 签发和刷新。
      • API 网关: 所有业务请求都通过 API 网关。网关的核心职责之一就是统一鉴权。它会拦截所有请求,解析 Authorization 头中的 Access Token,调用认证中心或自行验证 Token 的有效性。验证通过后,可以将解析出的用户信息(如用户ID)附加到请求头中,再转发给后端的业务微服务。
      • 业务微服务: 业务微服务本身不再关心 Token 的验证细节,它们信任来自网关的请求,直接从请求头中获取用户信息进行业务处理,实现了业务与认证的解耦。
      • 流程:
        1. Web/App 客户端引导用户到认证中心进行登录。
        2. 登录成功后,认证中心返回 Access TokenRefresh Token
        3. 客户端保存令牌,后续访问业务 API 时,在请求头携带 Access Token
        4. API 网关拦截请求,验证 Access Token
        5. Access Token 过期后,客户端使用 Refresh Token 向认证中心申请新令牌。
      • 这个架构具有高内聚、低耦合、可扩展性强、安全性高的优点。

Redis 详细技术解析

Redis 核心架构与原理

内存模型与数据结构

Redis采用基于内存的存储架构,所有数据都保存在RAM中,这是其高性能的根本原因。Redis使用了多种底层数据结构来实现上层的抽象数据类型:

SDS(Simple Dynamic String) Redis没有直接使用C语言的字符串,而是构建了SDS。SDS在字符串头部记录了长度信息,避免了strlen的O(n)复杂度,同时预分配空间减少内存重分配次数。

跳跃表(Skip List) 有序集合的底层实现之一,是一种随机化的数据结构,通过多层链表实现O(log N)的查找复杂度。相比红黑树,跳跃表实现更简单,且支持范围查询。

压缩列表(Ziplist) 当哈希、列表、有序集合元素较少时使用的紧凑存储结构,所有元素存储在一块连续内存中,节省内存但插入删除效率较低。

字典(Dict) Redis的核心数据结构,使用开放寻址法解决哈希冲突,支持渐进式rehash。当负载因子过高时,会创建新的哈希表并逐步迁移数据。

单线程模型与事件循环

Redis 6.0之前采用单线程模型处理客户端请求,通过I/O多路复用(epoll/kqueue)实现高并发。单线程避免了线程切换开销和并发控制问题,但也限制了CPU利用率。

事件循环机制 Redis使用Reactor模式的事件循环,分为文件事件和时间事件:

  • 文件事件:处理客户端连接、读写请求
  • 时间事件:处理定时任务,如过期键删除、持久化等

Redis 6.0引入了多线程I/O,但命令执行仍是单线程,多线程只用于网络I/O操作的读写,这样既提高了网络处理能力,又保持了数据操作的原子性。

数据类型详解与应用场景

String类型

String是Redis最基础的数据类型,底层可以是SDS、整数或浮点数。

常用命令:SET、GET、INCR、DECR、APPEND、GETRANGE 应用场景

  • 缓存:存储用户会话、配置信息
  • 计数器:网站访问量、点赞数(INCR原子性保证)
  • 分布式锁:SET key value NX EX seconds
  • 限流:结合EXPIRE实现滑动窗口限流

Hash类型

Hash类型适合存储对象,避免了将对象序列化为JSON字符串的开销。

底层实现:元素较少时使用ziplist,元素较多时使用hashtable 应用场景

  • 存储用户信息:HSET user:1001 name “张三” age 25
  • 购物车:HSET cart:1001 product:123 2
  • 配置管理:分类存储不同模块的配置

List类型

List是双向链表实现,支持在两端进行O(1)的插入和删除操作。

常用命令:LPUSH、RPUSH、LPOP、RPOP、LRANGE、BLPOP 应用场景

  • 消息队列:生产者LPUSH,消费者BRPOP实现阻塞队列
  • 最新列表:朋友圈动态、商品评论
  • 栈和队列:LPUSH+LPOP实现栈,LPUSH+RPOP实现队列

Set类型

Set是无序集合,元素唯一,底层使用hashtable或intset实现。

集合运算:SINTER(交集)、SUNION(并集)、SDIFF(差集) 应用场景

  • 去重:统计网站独立访客
  • 社交关系:共同好友、推荐用户
  • 标签系统:用户标签、文章分类
  • 抽奖系统:SRANDMEMBER随机抽取

Sorted Set类型

有序集合,每个元素关联一个分数,按分数排序。底层使用跳跃表和哈希表。

应用场景

  • 排行榜:游戏积分、热搜榜
  • 延时队列:分数为执行时间戳
  • 范围查询:按时间、按分数范围获取数据

持久化机制深度解析

RDB持久化

RDB通过fork子进程,将内存数据快照写入磁盘。

优点

  • 文件紧凑,适合备份和灾难恢复
  • 恢复速度快
  • 对Redis性能影响小(子进程操作)

缺点

  • 数据丢失风险:两次快照间的数据可能丢失
  • fork过程会阻塞主进程
  • 大数据集fork耗时较长

触发条件

  • 手动执行SAVE或BGSAVE命令
  • 配置自动触发:save 900 1(900秒内至少1个键改变)
  • 主从复制时自动生成RDB

AOF持久化

AOF记录每个写命令,通过重放命令恢复数据。

写入时机

  • always:每个写命令立即同步,安全但性能低
  • everysec:每秒同步一次,平衡安全性和性能
  • no:由操作系统决定同步时机,性能高但安全性低

AOF重写: AOF文件会越来越大,Redis提供重写机制优化:

  • 遍历内存数据,用最少命令重建AOF文件
  • 重写期间的新命令写入AOF重写缓冲区
  • 原子性替换旧AOF文件

混合持久化: Redis 4.0引入RDB+AOF混合模式,重写时以RDB格式写入基础数据,增量命令以AOF格式追加,兼顾了恢复速度和数据安全。

缓存问题与解决方案

缓存穿透

查询不存在的数据,缓存无法生效,请求直达数据库。

解决方案

  1. 空值缓存:查询结果为空时也缓存,设置较短过期时间
1
2
3
if (data == null) {
redis.setex(key, 60, "null"); // 缓存空值60秒
}
  1. 布隆过滤器:预先将所有可能存在的数据哈希到位数组
1
2
3
4
5
BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000000, 0.01);
// 查询前先检查布隆过滤器
if (!filter.mightContain(key)) {
return null; // 一定不存在
}
  1. 参数校验:在API层面进行参数合法性校验

缓存击穿

热点数据过期瞬间,大量并发请求击穿缓存。

解决方案

  1. 互斥锁:只允许一个线程查询数据库并重建缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String getData(String key) {
String data = redis.get(key);
if (data == null) {
String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1", 10)) { // 获取锁
try {
data = database.query(key); // 查询数据库
redis.setex(key, 3600, data); // 重建缓存
} finally {
redis.del(lockKey); // 释放锁
}
} else {
Thread.sleep(50); // 等待其他线程重建缓存
return getData(key); // 递归重试
}
}
return data;
}
  1. 热点数据永不过期:逻辑上设置过期时间,物理上不过期,异步更新
  2. 预热机制:系统启动时预先加载热点数据
  3. 二级缓存:L1缓存过期后,先返回L2缓存数据,异步更新L1

缓存雪崩

大量缓存同时过期或Redis宕机,请求涌向数据库。

解决方案

  1. 过期时间随机化:避免同时过期
1
2
int randomExpire = baseExpire + new Random().nextInt(300);  // 基础时间+随机时间
redis.setex(key, randomExpire, data);
  1. 多级缓存架构
    • L1:本地缓存(如Caffeine)
    • L2:Redis分布式缓存
    • L3:数据库
  2. 限流降级:使用Sentinel、Hystrix等组件
  3. Redis高可用:主从复制、哨兵模式、集群部署

缓存预热

系统启动时预先加载热点数据到缓存。

实现方式

  1. 定时任务预热:凌晨低峰期执行
  2. 手动预热:管理后台触发预热任务
  3. 实时预热:监控系统发现热点数据自动预热

数据一致性保证

Cache Aside模式(旁路缓存)

应用程序负责维护缓存和数据库的一致性。

读操作

  1. 先读缓存,命中则返回
  2. 缓存不命中,查询数据库
  3. 将数据写入缓存并返回

写操作

  1. 先更新数据库
  2. 删除缓存(让下次读取时重新加载)

为什么是删除而不是更新缓存?

  • 更新缓存可能存在并发问题
  • 复杂查询的缓存更新成本高
  • 删除缓存更简单可靠

延时双删策略

解决读写并发导致的数据不一致问题。

实现步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void updateData(String key, Object data) {
// 1. 删除缓存
redis.del(key);

// 2. 更新数据库
database.update(data);

// 3. 延时删除缓存(异步)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 延时500ms
redis.del(key); // 再次删除缓存
} catch (InterruptedException e) {
log.error("延时删除缓存失败", e);
}
});
}

延时时间设置:通常为主从同步时间 + 读数据库时间 + 几十毫秒

基于消息队列的最终一致性

使用消息队列异步处理缓存更新,保证最终一致性。

实现流程

  1. 更新数据库,发送消息到队列
  2. 消息消费者删除相关缓存
  3. 消费失败时重试,保证最终一致性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 发送缓存删除消息
@Transactional
public void updateUser(User user) {
userDao.update(user); // 更新数据库

// 发送缓存删除消息(事务提交后发送)
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
cacheDeleteMQ.send("user:" + user.getId());
}
}
);
}

分布式事务方案

对于强一致性要求高的场景,可以使用分布式事务。

2PC(两阶段提交)

  • 准备阶段:协调者询问参与者是否准备好
  • 提交阶段:所有参与者都准备好则提交,否则回滚

TCC(Try-Confirm-Cancel)

  • Try:尝试执行,预留资源
  • Confirm:确认提交
  • Cancel:取消执行,释放资源

分布式锁实现

基于SET命令的分布式锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RedisDistributedLock {
private Jedis jedis;

public boolean tryLock(String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}

public boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
return "1".equals(result.toString());
}
}

使用方式

1
2
3
4
5
6
7
8
9
10
11
String lockKey = "lock:user:1001";
String requestId = UUID.randomUUID().toString();

if (tryLock(lockKey, requestId, 30000)) {
try {
// 执行业务逻辑
doSomething();
} finally {
releaseLock(lockKey, requestId);
}
}

Redlock算法

为了解决单点故障问题,Redis官方提出了Redlock算法。

算法步骤

  1. 获取当前时间戳
  2. 依次向N个Redis实例申请锁
  3. 如果在大多数实例(N/2+1)上获取锁成功,且总耗时小于锁超时时间,则认为获取锁成功
  4. 锁的有效时间 = 初始有效时间 - 获取锁消耗的时间
  5. 释放锁时,向所有Redis实例发送释放命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Redlock {
private List<Jedis> jedisInstances;

public boolean lock(String resource, int ttl) {
int quorum = jedisInstances.size() / 2 + 1;
int successCount = 0;
long startTime = System.currentTimeMillis();

for (Jedis jedis : jedisInstances) {
if (lockInstance(jedis, resource, ttl)) {
successCount++;
}
}

long elapsedTime = System.currentTimeMillis() - startTime;
if (successCount >= quorum && elapsedTime < ttl) {
return true;
} else {
unlock(resource); // 释放已获取的锁
return false;
}
}
}

锁的问题与优化

锁超时问题: 业务执行时间超过锁超时时间,锁自动释放,可能导致并发问题。

解决方案

  1. 看门狗机制:定时续期锁的过期时间
1
2
3
4
5
6
7
8
9
10
11
12
13
public class WatchDog {
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

public void startWatchDog(String lockKey, String requestId) {
scheduler.scheduleAtFixedRate(() -> {
// 续期锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
jedis.eval(script, Arrays.asList(lockKey),
Arrays.asList(requestId, "30"));
}, 10, 10, TimeUnit.SECONDS);
}
}
  1. 合理评估业务执行时间:设置足够的锁超时时间

锁竞争激烈问题: 大量线程竞争同一把锁,导致性能下降。

解决方案

  1. 分段锁:将资源分段,减少锁竞争
  2. 队列锁:使用List实现公平锁
  3. 自旋锁优化:适当的退避算法

悲观锁与乐观锁

悲观锁

假设会发生并发冲突,在操作数据前先获取锁。

Redis实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用Redis分布式锁实现悲观锁
public void updateWithPessimisticLock(String userId, int amount) {
String lockKey = "lock:account:" + userId;
String requestId = UUID.randomUUID().toString();

if (tryLock(lockKey, requestId, 30000)) {
try {
// 查询账户余额
int balance = getBalance(userId);
if (balance >= amount) {
// 扣减余额
updateBalance(userId, balance - amount);
} else {
throw new InsufficientBalanceException();
}
} finally {
releaseLock(lockKey, requestId);
}
} else {
throw new LockAcquisitionException();
}
}

乐观锁

假设不会发生冲突,在更新时检查数据是否被修改。

基于版本号的乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public boolean updateWithOptimisticLock(String userId, int amount, int expectedVersion) {
// 使用Lua脚本保证原子性
String script =
"local current = redis.call('hmget', KEYS[1], 'balance', 'version') " +
"if current[2] == ARGV[2] then " +
" if tonumber(current[1]) >= tonumber(ARGV[1]) then " +
" redis.call('hmset', KEYS[1], 'balance', current[1] - ARGV[1], 'version', current[2] + 1) " +
" return 1 " +
" else " +
" return -1 " + // 余额不足
" end " +
"else " +
" return 0 " + // 版本号不匹配
"end";

String key = "account:" + userId;
Object result = jedis.eval(script, Arrays.asList(key),
Arrays.asList(String.valueOf(amount), String.valueOf(expectedVersion)));

int code = ((Long) result).intValue();
if (code == 1) {
return true; // 更新成功
} else if (code == 0) {
throw new OptimisticLockException("数据已被修改");
} else {
throw new InsufficientBalanceException("余额不足");
}
}

基于CAS的乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void updateWithCAS(String key, Function<String, String> updater) {
while (true) {
// 1. 获取当前值
String currentValue = redis.get(key);

// 2. 计算新值
String newValue = updater.apply(currentValue);

// 3. CAS更新
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('set', KEYS[1], ARGV[2]) else return nil end";
Object result = redis.eval(script, Arrays.asList(key),
Arrays.asList(currentValue, newValue));

if (result != null) {
break; // 更新成功
}
// 更新失败,重试
}
}

悲观锁 vs 乐观锁选择

悲观锁适用场景

  • 写操作频繁,冲突概率高
  • 业务逻辑复杂,重试成本高
  • 对数据一致性要求严格

乐观锁适用场景

  • 读多写少,冲突概率低
  • 业务逻辑简单,重试成本低
  • 对性能要求高

高可用架构

主从复制

Master负责写操作,Slave负责读操作,通过复制实现数据同步。

复制原理

  1. Slave向Master发送PSYNC命令
  2. Master执行BGSAVE生成RDB文件
  3. Master将RDB文件发送给Slave
  4. Slave载入RDB文件
  5. Master将缓冲区的写命令发送给Slave
  6. 后续写命令实时同步

部分重同步: 网络断连后,Slave只需要同步断连期间的命令,而不是完整重同步。

哨兵模式(Sentinel)

哨兵负责监控Master状态,在Master故障时自动进行故障转移。

哨兵职责

  • 监控:定期ping Master和Slave
  • 通知:故障时通知管理员和客户端
  • 故障转移:自动将Slave提升为新Master
  • 配置管理:为客户端提供服务发现

故障转移流程

  1. 哨兵发现Master下线(主观下线)
  2. 多个哨兵确认Master下线(客观下线)
  3. 选举领导哨兵执行故障转移
  4. 选择合适的Slave作为新Master
  5. 修改其他Slave的配置指向新Master
  6. 通知客户端Master地址变更

集群模式(Cluster)

Redis Cluster通过分片实现横向扩展和高可用。

分片算法: 使用CRC16算法计算key的哈希值,然后对16384取模得到槽位号。

节点通信: 使用Gossip协议在节点间交换状态信息,包括节点上线/下线、槽位分配等。

故障转移: 当Master节点故障时,其Slave自动提升为新Master,过程对客户端透明。

数据迁移: 集群扩容时,需要将部分槽位从现有节点迁移到新节点。

性能监控与优化

慢查询日志

Redis提供慢查询日志功能,记录执行时间超过阈值的命令。

配置参数

1
2
slowlog-log-slower-than 10000  # 超过10毫秒记录
slowlog-max-len 128 # 最多保存128条记录

查看慢查询

1
SLOWLOG GET 10  # 获取最近10条慢查询

内存分析

使用MEMORY命令分析内存使用情况。

1
2
3
MEMORY USAGE key         # 查看key占用内存
MEMORY STATS # 查看内存统计信息
MEMORY DOCTOR # 内存使用建议

性能优化建议

避免大key

  • 单个key的value不要超过10KB
  • 集合类型元素数量控制在合理范围
  • 使用SCAN代替KEYS命令

合理使用数据结构

  • 小对象使用Hash而不是多个String
  • 合理设置ziplist等压缩结构的阈值
  • 使用位图(bitmap)存储布尔类型大数据集

网络优化

  • 使用Pipeline批量操作
  • 合理设置客户端连接池
  • 启用TCP_NODELAY选项

持久化优化

  • 根据业务需求选择RDB或AOF
  • 合理配置自动保存条件
  • 在从节点上进行持久化操作

这些详细的技术点涵盖了Redis的核心概念、常见问题解决方案和实际应用场景,是Redis技术面试的重要考查内容。掌握这些知识点并能结合实际项目经验进行说明,将大大提高面试通过率。

内存管理与淘汰机制

内存淘汰策略详解

当Redis内存使用达到maxmemory限制时,会根据配置的策略淘汰数据。

8种淘汰策略

1
2
3
4
5
6
7
8
9
10
11
# 针对所有key
noeviction # 不淘汰,写入返回错误
allkeys-lru # 所有key中淘汰最近最少使用
allkeys-lfu # 所有key中淘汰最少频率使用
allkeys-random # 所有key中随机淘汰

# 针对设置了过期时间的key
volatile-lru # 过期key中淘汰最近最少使用
volatile-lfu # 过期key中淘汰最少频率使用
volatile-random # 过期key中随机淘汰
volatile-ttl # 过期key中淘汰即将过期的

LRU vs LFU 实现细节: Redis的LRU并非严格的LRU,而是近似LRU算法:

  • 每个key都有24位的时钟字段记录访问时间
  • 淘汰时随机采样5个key(可配置),选择时钟值最小的

LFU算法维护访问频率:

  • 高16位存储上次访问时间
  • 低8位存储访问频率计数器
  • 计数器采用概率性递增,避免频率无限增长

内存碎片问题

产生原因

  • 频繁的数据更新导致内存分配/释放
  • Redis使用jemalloc内存分配器,存在内存对齐
  • 删除大key后留下内存空洞

检测方法

1
2
3
4
INFO memory
# 关注 mem_fragmentation_ratio 指标
# 比值 > 1.5 表示碎片较多
# 比值 < 1 表示使用了swap,性能严重下降

解决方案

  1. 内存整理(Redis 4.0+):
1
2
3
CONFIG SET activedefrag yes        # 开启自动整理
CONFIG SET active-defrag-threshold-lower 10 # 碎片率超过10%启动
MEMORY PURGE # 手动触发整理
  1. 重启Redis:最彻底但影响服务可用性
  2. 优化数据结构:减少小对象,使用Hash存储相关数据

过期策略与删除机制

三种过期删除策略

定时删除:设置过期时间时创建定时器,到期立即删除

  • 优点:及时释放内存
  • 缺点:消耗CPU资源创建和管理定时器

惰性删除:访问key时检查是否过期,过期则删除

  • 优点:CPU友好,只在必要时删除
  • 缺点:内存不友好,过期key可能长期占用内存

定期删除:定期随机检查部分key,删除过期的

  • Redis的实际策略,平衡CPU和内存使用

Redis过期删除实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 简化的过期删除逻辑
void activeExpireCycle(int type) {
static int current_db = 0;
static int timelimit_exit = 0;

for (int i = 0; i < server.dbnum; i++) {
redisDb *db = server.db + current_db % server.dbnum;

int iteration = 0;
while (iteration < 20) { // 最多检查20个key
long sampled = 0, expired = 0;

// 随机选择key检查过期
for (int i = 0; i < 20; i++) {
dictEntry *de = dictGetRandomKey(db->expires);
if (de == NULL) break;

sampled++;
if (keyIsExpired(db, de)) {
deleteExpiredKey(db, de);
expired++;
}
}

// 如果过期比例超过25%,继续下轮检查
if (expired > 5) iteration++;
else break;
}
current_db++;
}
}

数据结构底层实现深度解析

压缩列表(ZipList)演进

Redis 7.0用ListPack替代了ZipList,解决了级联更新问题。

ZipList问题

1
2
3
// ZipList结构导致的级联更新
// 当插入元素导致某个entry的长度改变时
// 可能引起后续所有entry的重新编码

ListPack优势

  • 每个元素独立编码,避免级联更新
  • 支持从尾部遍历,提高某些操作效率

字典扩容与rehash

Redis字典使用增量式rehash避免阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 渐进式rehash实现
int dictRehash(dict *d, int n) {
int empty_visits = n * 10;

if (!dictIsRehashing(d)) return 0;

while (n-- && d->ht[0].used != 0) {
// 跳过空桶
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}

// 迁移桶中所有元素
dictEntry *de = d->ht[0].table[d->rehashidx];
while (de) {
dictEntry *nextde = de->next;
unsigned int h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}

return 0;
}

网络模型与性能优化

Redis 6.0 多线程I/O

多线程只用于网络I/O,命令执行仍是单线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 多线程I/O处理流程
void *IOThreadMain(void *myid) {
while (1) {
// 等待主线程分配任务
pthread_mutex_lock(&io_threads_mutex[id]);

while (io_threads_list[id] && io_threads_pending[id] == 0) {
pthread_cond_wait(&io_threads_cond[id], &io_threads_mutex[id]);
}

// 处理读写任务
listNode *ln;
listIter li;
listRewind(io_threads_list[id], &li);

while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (io_threads_op == IO_THREADS_OP_WRITE) {
writeToClient(c, 0);
} else if (io_threads_op == IO_THREADS_OP_READ) {
readQueryFromClient(c->conn);
}
}

io_threads_pending[id] = 0;
pthread_mutex_unlock(&io_threads_mutex[id]);
}
}

客户端连接管理

连接池配置优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// Jedis连接池配置
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200); // 最大连接数
config.setMaxIdle(50); // 最大空闲连接
config.setMinIdle(10); // 最小空闲连接
config.setTestOnBorrow(true); // 获取连接时测试
config.setTestOnReturn(true); // 归还连接时测试
config.setTestWhileIdle(true); // 空闲时测试
config.setTimeBetweenEvictionRunsMillis(30000); // 检查间隔
config.setNumTestsPerEvictionRun(10); // 每次检查连接数
config.setMinEvictableIdleTimeMillis(60000); // 最小空闲时间

JedisPool pool = new JedisPool(config, "localhost", 6379);

Pipeline优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// Pipeline批量操作
public void batchSet(Map<String, String> data) {
try (Jedis jedis = pool.getResource()) {
Pipeline pipeline = jedis.pipelined();

for (Map.Entry<String, String> entry : data.entrySet()) {
pipeline.set(entry.getKey(), entry.getValue());
}

List<Object> results = pipeline.syncAndReturnAll();
// 处理结果
}
}

高级数据类型与应用

HyperLogLog

用于基数统计,占用内存固定(12KB),误差率0.81%。

实现原理

  • 基于概率算法,通过观察随机数的最大前导零个数估算基数
  • 使用调和平均数减少误差
  • 适用于UV统计等场景
1
2
3
4
# 网站UV统计
PFADD uv:20230815 user1 user2 user3
PFCOUNT uv:20230815 # 获取UV数量
PFMERGE uv:week uv:20230815 uv:20230816 # 合并多日数据

布隆过滤器(Redis Module)

1
2
3
4
5
6
# Redis布隆过滤器模块
BF.RESERVE myfilter 0.01 1000000 # 创建过滤器,误报率0.01%,预期元素100万
BF.ADD myfilter "user123" # 添加元素
BF.EXISTS myfilter "user123" # 检查元素是否存在
BF.MADD myfilter item1 item2 item3 # 批量添加
BF.MEXISTS myfilter item1 item2 item3 # 批量检查

Geo地理位置

基于Sorted Set实现,使用GeoHash算法。

1
2
3
4
5
6
7
8
9
10
11
# 添加地理位置
GEOADD cities 116.397128 39.916527 "北京" 121.473701 31.230416 "上海"

# 计算距离
GEODIST cities "北京" "上海" km

# 范围查询
GEORADIUS cities 116.397128 39.916527 1000 km WITHDIST WITHCOORD

# 根据成员查询
GEORADIUSBYMEMBER cities "北京" 1000 km

安全性问题与防护

常见安全漏洞

命令注入

1
2
3
4
5
6
// 错误做法:直接拼接用户输入
String key = "user:" + userInput; // userInput可能包含恶意命令
jedis.eval("return redis.call('get', '" + key + "')", 0);

// 正确做法:使用参数化查询
jedis.eval("return redis.call('get', KEYS[1])", 1, key);

未授权访问

1
2
3
4
5
6
# redis.conf安全配置
bind 127.0.0.1 # 绑定指定IP
requirepass your_password # 设置密码
protected-mode yes # 开启保护模式
port 0 # 禁用默认端口
port 6380 # 使用非标准端口

危险命令禁用

1
2
3
4
5
# 重命名危险命令
rename-command FLUSHDB "" # 禁用FLUSHDB
rename-command FLUSHALL "" # 禁用FLUSHALL
rename-command SHUTDOWN "REDIS_SHUTDOWN" # 重命名SHUTDOWN
rename-command CONFIG "REDIS_CONFIG" # 重命名CONFIG

ACL访问控制(Redis 6.0+)

1
2
3
4
5
6
7
8
9
10
11
# 创建用户
ACL SETUSER alice on >password123 ~cached:* +get +set

# 查看用户权限
ACL LIST

# 删除用户
ACL DELUSER alice

# 检查权限
AUTH alice password123

监控与运维

关键监控指标

性能指标

1
2
3
4
5
6
7
8
# 通过INFO命令获取
INFO stats
# 关注指标:
# - instantaneous_ops_per_sec: QPS
# - keyspace_hits/keyspace_misses: 命中率
# - used_cpu_sys/used_cpu_user: CPU使用率
# - connected_clients: 连接数
# - blocked_clients: 阻塞连接数

内存指标

1
2
3
4
5
6
INFO memory
# 关注指标:
# - used_memory: 已使用内存
# - used_memory_rss: 物理内存占用
# - mem_fragmentation_ratio: 内存碎片率
# - used_memory_peak: 内存使用峰值

持久化指标

1
2
3
4
5
INFO persistence
# 关注指标:
# - rdb_last_save_time: 最后RDB保存时间
# - aof_last_rewrite_time_sec: AOF重写耗时
# - aof_current_size: AOF文件大小

故障排查常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 客户端连接信息
CLIENT LIST
CLIENT INFO

# 慢查询分析
SLOWLOG GET 10
CONFIG GET slowlog-log-slower-than

# 大key查找
MEMORY USAGE keyname
redis-cli --bigkeys

# 热点key分析
redis-cli --hotkeys

# 延迟监控
LATENCY HISTORY command
CONFIG SET latency-monitor-threshold 100

特殊应用场景

分布式限流

固定窗口限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RedisRateLimiter {
public boolean isAllowed(String key, int limit, int window) {
long current = System.currentTimeMillis() / 1000 / window;
String windowKey = key + ":" + current;

String script =
"local count = redis.call('incr', KEYS[1]) " +
"if count == 1 then " +
" redis.call('expire', KEYS[1], ARGV[1]) " +
"end " +
"return count";

Long count = (Long) jedis.eval(script, Arrays.asList(windowKey),
Arrays.asList(String.valueOf(window)));

return count <= limit;
}
}

滑动窗口限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean slidingWindowLimit(String key, int limit, int window) {
long now = System.currentTimeMillis();
long windowStart = now - window * 1000;

String script =
"redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +
"local count = redis.call('zcard', KEYS[1]) " +
"if count < tonumber(ARGV[2]) then " +
" redis.call('zadd', KEYS[1], ARGV[3], ARGV[3]) " +
" redis.call('expire', KEYS[1], ARGV[4]) " +
" return 1 " +
"else " +
" return 0 " +
"end";

Long result = (Long) jedis.eval(script, Arrays.asList(key),
Arrays.asList(String.valueOf(windowStart), String.valueOf(limit),
String.valueOf(now), String.valueOf(window)));

return result == 1;
}

分布式Session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class RedisSessionManager {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

public void createSession(String sessionId, String userId, int timeout) {
String key = "session:" + sessionId;
Map<String, Object> sessionData = new HashMap<>();
sessionData.put("userId", userId);
sessionData.put("createTime", System.currentTimeMillis());

redisTemplate.opsForHash().putAll(key, sessionData);
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}

public boolean isValidSession(String sessionId) {
String key = "session:" + sessionId;
return redisTemplate.hasKey(key);
}

public void renewSession(String sessionId, int timeout) {
String key = "session:" + sessionId;
if (redisTemplate.hasKey(key)) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}
}

消息队列实现

基于List的简单队列

1
2
3
4
5
6
7
8
9
10
11
12
public class RedisQueue {
// 生产者
public void produce(String queue, String message) {
jedis.lpush(queue, message);
}

// 消费者(阻塞式)
public String consume(String queue, int timeout) {
List<String> result = jedis.brpop(timeout, queue);
return result != null ? result.get(1) : null;
}
}

基于Stream的消息队列(Redis 5.0+):

1
2
3
4
5
6
7
8
9
10
11
# 生产消息
XADD mystream * field1 value1 field2 value2

# 创建消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM

# 消费消息
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS mystream >

# 确认消息
XACK mystream mygroup message_id

延时队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RedisDelayQueue {

public void addDelayTask(String task, long delayTime) {
long executeTime = System.currentTimeMillis() + delayTime;
jedis.zadd("delay_queue", executeTime, task);
}

public List<String> getReadyTasks() {
long now = System.currentTimeMillis();
Set<String> tasks = jedis.zrangeByScore("delay_queue", 0, now);

if (!tasks.isEmpty()) {
// 原子性删除已获取的任务
String script =
"local tasks = redis.call('zrangebyscore', KEYS[1], 0, ARGV[1]) " +
"if #tasks > 0 then " +
" redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +
"end " +
"return tasks";

@SuppressWarnings("unchecked")
List<String> result = (List<String>) jedis.eval(script,
Arrays.asList("delay_queue"), Arrays.asList(String.valueOf(now)));
return result;
}

return Collections.emptyList();
}
}

性能调优技巧

内存优化

使用Hash存储小对象

1
2
3
4
5
6
7
8
9
// 不推荐:为每个用户字段单独设置key
jedis.set("user:1001:name", "张三");
jedis.set("user:1001:age", "25");
jedis.set("user:1001:email", "zhang@example.com");

// 推荐:使用Hash存储
jedis.hset("user:1001", "name", "张三");
jedis.hset("user:1001", "age", "25");
jedis.hset("user:1001", "email", "zhang@example.com");

合理设置过期时间

1
2
3
4
// 避免同时过期导致雪崩
int baseExpire = 3600; // 1小时
int randomExpire = baseExpire + new Random().nextInt(300); // 增加0-5分钟随机时间
jedis.setex(key, randomExpire, value);

网络优化

批量操作优化

1
2
3
4
5
6
7
8
9
10
11
// 使用Pipeline
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipeline.set("key" + i, "value" + i);
}
pipeline.sync();

// 或使用原生批量命令
Map<String, String> batch = new HashMap<>();
// ... 填充数据
jedis.mset(flattenMap(batch));

CPU优化

Lua脚本减少网络往返

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 原子性增加库存
local current = redis.call('get', KEYS[1])
if current == false then
return -1 -- key不存在
end

current = tonumber(current)
if current >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1])
return current - tonumber(ARGV[1])
else
return -2 -- 库存不足
end

你好,我很乐意为你详细讲解 Redis。

Redis 是一个开源的、基于内存的、高性能的键值存储系统。它通常被用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串、哈希、列表、集合和有序集合,这使得它在各种应用场景中都非常灵活。

Redis 的核心特点


  • 高性能: Redis 的所有数据都存储在内存中,因此读写速度非常快。通常,单个 Redis 实例每秒能处理超过 10 万次读写操作。
  • 丰富的数据结构: Redis 不仅仅是简单的键值存储,它支持多种复杂的数据结构,这让它能满足更广泛的应用需求。
  • 持久化: 虽然 Redis 是基于内存的,但它提供了两种不同的持久化方式,可以将数据从内存中保存到磁盘上,以防止服务器重启后数据丢失。
  • 原子性: Redis 的所有操作都是原子性的。这意味着一个命令要么完全执行,要么根本不执行,中间不会被打断,这在多客户端并发访问时尤其重要。
  • 主从复制: Redis 支持主从复制,可以将数据从一个主节点(Master)同步到一个或多个从节点(Slave),从而实现数据的备份和读写分离,提高系统的可用性和扩展性。
  • 高可用性与集群: Redis 提供了 Sentinel(哨兵)和 Cluster(集群)两种方案,来保证系统的高可用性和扩展性。

Redis 的数据结构详解


这是 Redis 最有特色的地方,理解这些数据结构是掌握 Redis 的关键。

1. String (字符串)

  • 最基本的数据结构,可以存储任何类型的数据,比如字符串、数字或二进制数据。一个键最多可以存储 512MB 的值。
  • 常用命令:
    • SET key value: 设置键值对。
    • GET key: 获取键对应的值。
    • INCR key: 将键对应的值加 1,常用于计数器。
    • MSET key1 value1 key2 value2: 同时设置多个键值对。
    • MGET key1 key2: 同时获取多个键的值。
  • 应用场景: 缓存、计数器、分布式锁等。

2. Hash (哈希)

  • 类似于一个键值对的集合。一个哈希键可以存储多个字段(field)和对应的值(value),非常适合存储对象。
  • 常用命令:
    • HSET key field value: 设置哈希键中一个字段的值。
    • HGET key field: 获取哈希键中一个字段的值。
    • HGETALL key: 获取哈希键中所有的字段和值。
    • HMSET key field1 value1 field2 value2: 同时设置多个字段。
    • HDEL key field1 field2: 删除一个或多个字段。
  • 应用场景: 存储用户信息(如user:100作为键,nameagecity作为字段),商品信息等。

3. List (列表)

  • 一个有序的、可以重复的字符串集合。可以从列表的头部或尾部添加或删除元素。
  • 常用命令:
    • LPUSH key value1 value2: 从列表的头部插入一个或多个值。
    • RPUSH key value1 value2: 从列表的尾部插入一个或多个值。
    • LPOP key: 从列表的头部移除并返回一个值。
    • RPOP key: 从列表的尾部移除并返回一个值。
    • LRANGE key start stop: 获取列表中指定范围的元素。
  • 应用场景: 消息队列(队列、栈)、最新文章列表、历史记录等。

4. Set (集合)

  • 一个无序的、不重复的字符串集合。集合提供了交集、并集和差集等操作。
  • 常用命令:
    • SADD key member1 member2: 向集合中添加一个或多个成员。
    • SMEMBERS key: 获取集合中的所有成员。
    • SISMEMBER key member: 判断一个成员是否在集合中。
    • SINTER key1 key2: 求两个集合的交集。
    • SUNION key1 key2: 求两个集合的并集。
  • 应用场景: 社交网络中的共同好友、标签系统、去重等。

5. Sorted Set (有序集合)

  • 和集合类似,但每个成员都会关联一个分数(score)。通过这个分数,可以对集合中的成员进行排序。分数可以重复,但成员必须唯一。
  • 常用命令:
    • ZADD key score1 member1 score2 member2: 向有序集合中添加一个或多个带分数的成员。
    • ZRANGE key start stop: 按分数从小到大排序,获取指定范围的成员。
    • ZREVRANGE key start stop: 按分数从大到小排序,获取指定范围的成员。
    • ZSCORE key member: 获取一个成员的分数。
    • ZINCRBY key increment member: 给指定成员的分数增加指定值。
  • 应用场景: 排行榜(游戏分数、热点新闻)、带权重的任务队列等。

Redis 的持久化机制


Redis 提供了两种持久化方式,可以将数据从内存保存到硬盘,确保数据不会因为服务重启而丢失。

1. RDB (Redis Database)

  • 快照方式。它会在指定的时间间隔内,将内存中的数据快照写入到磁盘上一个二进制文件中(dump.rdb)。
  • 优点: RDB 文件是一个非常紧凑的二进制文件,非常适合备份和灾难恢复。
  • 缺点: 每次保存都会丢失从上次快照到现在之间的数据。

2. AOF (Append Only File)

  • 日志方式。它会记录每一次对 Redis 数据库的写操作命令,并以追加(append)的方式写入到文件中。当 Redis 重启时,会重新执行 AOF 文件中的命令来恢复数据。
  • 优点: 数据丢失风险低,可以配置为每秒同步一次,或者每执行一个命令就同步一次。
  • 缺点: AOF 文件通常比 RDB 文件大,且恢复速度可能较慢。

在实际应用中,通常会同时使用 RDB 和 AOF,以获得更高的可靠性。

Redis 的高可用性


1. 主从复制 (Replication)

  • 基本原理: 将一个 Redis 实例(主节点)的数据复制到一个或多个其他实例(从节点)。从节点的数据是主节点的完整副本。
  • 作用:
    • 数据备份: 从节点可以作为数据的热备份。
    • 读写分离: 大多数读操作可以分流到从节点上,减轻主节点的压力。

2. Sentinel (哨兵)

  • 作用: 自动化管理主从复制集群。它是一个监控系统,可以监控主节点和从节点是否正常运行。
  • 核心功能:
    • 监控: 不断检查主从节点是否正常工作。
    • 通知: 当某个 Redis 实例出现问题时,会发送通知。
    • 故障转移: 如果主节点发生故障,Sentinel 会自动从剩下的从节点中选举一个新的主节点,并让其他从节点切换到这个新的主节点上,从而实现高可用。

3. Redis Cluster (集群)

  • 作用: 解决 Redis 单机内存容量和并发量的瓶颈问题。它将数据分散到多个节点上,每个节点只负责存储部分数据。
  • 核心功能:
    • 数据分片: 自动将数据分布在多个节点上。
    • 高可用性: 即使部分节点宕机,集群也能继续正常工作。