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")
注解。
- 作用: Feign 让远程服务调用变得像调用本地方法一样简单。开发者只需定义一个接口,并使用
- 熔断与降级 (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 的服务发现,并提供更实时的健康检查机制。
- 配置管理: 可以作为分布式配置中心,对所有微服务的配置进行集中化管理。支持配置的热更新,即修改配置后无需重启服务即可生效。还支持配置的版本管理、灰度发布等高级功能。
- 使用:
- 引入
spring-cloud-starter-alibaba-nacos-discovery
和spring-cloud-starter-alibaba-nacos-config
依赖。 - 在
bootstrap.properties
(或.yml
) 文件中配置 Nacos 服务器地址和应用名。 - 使用
@Value
或@ConfigurationProperties
注解即可动态获取和刷新配置。
- 引入
- 作用: Nacos (Naming and Configuration Service) 是一个功能丰富的平台,完美整合了服务注册发现和配置管理两大核心功能。
- 熔断、降级与流量控制: Sentinel
- 作用: Sentinel 是面向分布式服务架构的“流量的守护者”,以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。相较于 Hystrix,Sentinel 功能更强大,提供了可视化的监控和配置平台,并且支持热点参数限流等精细化控制。
- 核心概念:
- 资源 (Resource): Sentinel 中一切皆资源,可以是一个方法、一段代码或一个服务 URL。
- 规则 (Rule): 定义如何保护资源,包括流控规则、降级规则、系统保护规则等。
- 使用:
- 引入
spring-cloud-starter-alibaba-sentinel
依赖。 - 配置 Sentinel 控制台地址。
- 通过
@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:create
、order:delete
、article: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_id
和role_id
的联合主键)
- 角色-权限关联表 (t_role_permission): 存储角色和权限的多对多关系。
role_id
(外键),permission_id
(外键)。(主键是role_id
和permission_id
的联合主键)
4. Redis 进阶详解
核心特点
- 基于内存: Redis 是一个内存数据结构存储系统,所有数据都存放在内存中,因此读写速度极快。
- 单线程模型: Redis 的核心网络模型处理客户端请求是单线程的。这避免了多线程环境下的上下文切换和锁竞争带来的开销。(注意:Redis 6.0 之后引入了多线程来处理 I/O,但执行命令的核心仍然是单线程)。
- IO多路复用: 单线程能实现高性能的关键在于它使用了 IO 多路复用技术(如 Linux 下的 epoll)。该技术允许单个线程同时监听多个网络连接上的 IO 事件,当某个连接准备好读或写时,才去处理它,极大地提高了并发处理能力。
- 丰富的数据类型: 支持 String, Hash, List, Set, Sorted Set, Bitmap, HyperLogLog, GEO 等多种数据结构。
缓存三大问题与解决方案
- 缓存穿透:
- 问题: 客户端查询一个数据库和缓存中都不存在的数据。这导致每次请求都会绕过缓存,直接打到数据库上,当有大量此类请求时,会给数据库带来巨大压力。
- 解决:
- 缓存空对象: 当从数据库查询不到数据时,也在缓存中存入一个特殊的空值(如
null
或特定字符串),并设置一个较短的过期时间。 - 布隆过滤器 (Bloom Filter): 在访问缓存之前,先通过布隆过滤器判断 key 是否可能存在。布隆过滤器可以高效地判断一个元素一定不存在,从而在第一层就拦截掉大量无效请求。
- 缓存空对象: 当从数据库查询不到数据时,也在缓存中存入一个特殊的空值(如
- 缓存击穿:
- 问题: 某个热点 Key 在某一时刻突然失效,而此时恰好有大量的并发请求访问这个 Key,这些请求会同时穿透缓存,直接打到数据库上,可能导致数据库瞬间崩溃。
- 解决:
- 设置热点 Key 永不过期: 对于一些访问极其频繁且数据相对固定的热点数据,可以考虑不设置过期时间,通过后台任务定时更新。
- 使用分布式锁: 在查询数据库之前,先获取一个该 Key 对应的分布式锁。只有第一个获取到锁的线程才能去查询数据库并回写缓存,其他线程则等待或直接返回。
- 缓存雪崩:
- 问题: 大量的缓存 Key 在同一时间集中失效(例如,在应用启动时缓存了大量数据,并设置了相同的过期时间),导致所有请求都瞬间涌向数据库,造成数据库压力剧增甚至宕机。
- 解决:
- 过期时间加随机值: 在设置缓存的过期时间时,在一个基础时间上增加一个随机数,使得 Key 的失效时间点尽可能分散。
- 多级缓存: 建立多级缓存体系,如 Nginx 缓存 + Redis 缓存 + JVM 本地缓存(Caffeine/Guava Cache)。
- 熔断降级: 使用 Hystrix 或 Sentinel 等组件,当检测到数据库压力过大时,进行熔断或降级处理,暂时不访问数据库,返回一个默认值或提示信息。
5. 消息队列(MQ)
核心作用
- 异步 (Asynchronous): 将耗时的操作(如发送邮件、生成报表)作为消息放入 MQ,主流程可以立即返回,无需等待这些操作完成,从而提高系统的响应速度和吞吐量。
- 解耦 (Decoupling): 生产者和消费者之间通过 MQ 进行通信,无需直接相互依赖。任何一方的修改、宕机或升级都不会影响到另一方,增强了系统的灵活性和可维护性。
- 削峰 (Peak Shaving): 在秒杀、大促等高并发场景下,将瞬时涌入的大量请求暂存在 MQ 中,下游的消费者系统可以按照自己的处理能力,平稳地从 MQ 中拉取并处理请求,避免了流量洪峰直接冲垮下游服务。
面试题:“请列举你使用消息队列时可能遇到的问题,并给出解决方案。”
回答要点:
- 消息丢失 (Message Loss):
- 问题: 消息从生产者发出后,由于网络或 MQ 服务故障,未能成功到达消费者。
- 解决:
- 生产者端: 开启生产者的
confirm
或ack
机制,确保消息被 MQ 成功接收。如果发送失败,可以进行重试。 - MQ 服务端: 对消息进行持久化,防止 MQ 宕机导致消息丢失(如 RabbitMQ 的持久化队列和消息,Kafka 的磁盘存储)。
- 消费者端: 消费者在处理完业务逻辑后,再向 MQ 发送确认应答(
ack
)。如果处理过程中消费者宕机,MQ 没有收到ack
,会将该消息重新投递给其他消费者。
- 生产者端: 开启生产者的
- 重复消费 (Duplicate Consumption):
- 问题: 由于网络抖动、消费者
ack
超时等原因,MQ 可能会重复投递同一条消息。 - 解决: 核心是保证消费逻辑的幂等性 (Idempotence)。即多次执行同一个操作,结果都是相同的。实现方式有:
- 在数据库中为业务操作创建一个唯一键,每次操作前先检查该唯一键是否存在。
- 使用一个全局唯一的业务 ID(如订单号),在处理消息前,先查询这个 ID 是否已经被处理过(例如,存入 Redis Set 或数据库)。
- 问题: 由于网络抖动、消费者
- 消息堆积 (Message Backlog):
- 问题: 生产者的生产速度远大于消费者的消费速度,导致大量消息在 MQ 中积压,占用资源并可能导致消息超时丢失。
- 解决:
- 水平扩展消费者: 增加消费者实例的数量,并行处理消息。这是最常用的方法。
- 优化消费逻辑: 检查消费者代码,看是否有可以优化的慢操作(如 I/O 密集型操作)。
- 消息分片/分区: 对 Topic 进行分区(Partitioning),让不同的消费者组处理不同的分区,提高并发度。
- 增加预警监控: 对消息堆积数量设置阈值,达到阈值时及时告警,人工介入。
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) 的实现。它不是真正的加锁,而是在更新数据时检查数据是否被修改过。
使用场景: 适用于读多写少的并发场景,可以减少锁的开销。例如,更新商品库存。
WATCH stock_key
// 监视库存current_stock = GET stock_key
// 获取当前库存(在客户端代码中判断
current_stock
是否足够)MULTI
// 开启事务SET stock_key new_stock
// 准备更新库存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):
- 准备阶段 (Prepare): TM 通知所有 RM 准备提交,RM 执行本地事务并锁定资源,但不提交,然后向 TM 报告准备就绪。
- 提交/回滚阶段 (Commit/Rollback): 如果所有 RM 都准备就绪,TM 通知所有 RM 提交;否则,通知所有 RM 回滚。
- 评价: 是一种强一致性的方案,但协议复杂,性能差,同步阻塞模型会长时间锁定资源,且协调器存在单点故障风险,在互联网高并发场景下很少使用。
- TCC (Try-Confirm-Cancel): 补偿型事务。
- 核心: 是一种业务层面的柔性事务方案,对代码侵入性强。
- 流程:
- Try: 对业务资源进行检查和预留。例如,扣减库存操作,Try 阶段是冻结库存。
- Confirm: 如果所有服务的 Try 阶段都成功,则执行所有服务的 Confirm 操作,真正完成业务。例如,将冻结的库存真实扣减。
- 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 | -- 脚本逻辑:检查库存是否充足,如果充足则扣减并返回1,否则返回0 |
这个脚本保证了“读取库存”和“扣减库存”两个操作的原子性,避免了在高并发下超卖的问题。
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 认证原理(工作流程)
- 登录: 用户使用用户名和密码发起登录请求。
- 验证: 服务端验证用户的凭据是否正确。
- 签发 Token: 验证成功后,服务端根据用户ID、角色等信息,结合密钥(secret),生成一个 Token。
- 返回 Token: 服务端将生成的 Token 返回给客户端。
- 存储 Token: 客户端(浏览器、App)将 Token 存储起来,通常放在
localStorage
、sessionStorage
或HttpOnly
的 Cookie 中。 - 携带 Token 请求: 客户端在后续每次请求受保护的 API 时,都会在 HTTP 请求头的
Authorization
字段中附上 Token,格式通常为Bearer <token>
。 - 验证 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。
- 双令牌工作流程(静默刷新)
- 首次登录: 用户登录成功,服务端返回一个短期的
Access Token
和一个长期的Refresh Token
。客户端将两者都存储起来。 - 正常访问: 客户端使用
Access Token
访问 API。服务端验证Access Token
通过,返回数据。 - Access Token 过期: 客户端再次使用过期的
Access Token
访问 API,服务端返回401 Unauthorized
错误,并可能带上一个特定错误码,告知客户端是“令牌过期”而非“无效令牌”。 - 静默刷新: 客户端的请求拦截器捕获到这个
401
错误后,不会立即跳转到登录页。而是自动发起一个特殊的请求,携带那个长期的Refresh Token
去访问一个专门的刷新接口(如/api/token/refresh
)。 - 签发新令牌: 服务端验证
Refresh Token
的有效性(通常会将其存储在 Redis 或数据库中进行比对,以实现强制下线功能)。如果验证通过,就生成一个新的 Access Token(有时也会生成一个新的Refresh Token
,这被称为刷新令牌滚动策略)并返回给客户端。 - 重试请求: 客户端收到新的
Access Token
后,用它替换掉本地旧的Access Token
,然后自动重新发送刚才因令牌过期而失败的那个请求。 - 无感体验: 整个过程对用户是透明的,用户感觉不到令牌已经过期并被刷新,实现了“静默刷新”,体验非常流畅。
- 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 漏洞的出现。
- 高安全性方案: 将
- localStorage/sessionStorage:
- 回答要点:
- 面试题 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
过期后,自然就下线了。
- 黑名单机制: 服务端可以建立一个 Token 黑名单(例如,使用 Redis Set)。当用户退出登录时,将该 Token 的
- 对于无状态的 JWT: 由于所有信息都在 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 的验证细节,它们信任来自网关的请求,直接从请求头中获取用户信息进行业务处理,实现了业务与认证的解耦。
- 流程:
- Web/App 客户端引导用户到认证中心进行登录。
- 登录成功后,认证中心返回
Access Token
和Refresh Token
。 - 客户端保存令牌,后续访问业务 API 时,在请求头携带
Access Token
。 - API 网关拦截请求,验证
Access Token
。 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 | if (data == null) { |
- 布隆过滤器:预先将所有可能存在的数据哈希到位数组
1 | BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000000, 0.01); |
- 参数校验:在API层面进行参数合法性校验
缓存击穿
热点数据过期瞬间,大量并发请求击穿缓存。
解决方案:
- 互斥锁:只允许一个线程查询数据库并重建缓存
1 | public String getData(String key) { |
- 热点数据永不过期:逻辑上设置过期时间,物理上不过期,异步更新
- 预热机制:系统启动时预先加载热点数据
- 二级缓存:L1缓存过期后,先返回L2缓存数据,异步更新L1
缓存雪崩
大量缓存同时过期或Redis宕机,请求涌向数据库。
解决方案:
- 过期时间随机化:避免同时过期
1 | int randomExpire = baseExpire + new Random().nextInt(300); // 基础时间+随机时间 |
- 多级缓存架构:
- L1:本地缓存(如Caffeine)
- L2:Redis分布式缓存
- L3:数据库
- 限流降级:使用Sentinel、Hystrix等组件
- Redis高可用:主从复制、哨兵模式、集群部署
缓存预热
系统启动时预先加载热点数据到缓存。
实现方式:
- 定时任务预热:凌晨低峰期执行
- 手动预热:管理后台触发预热任务
- 实时预热:监控系统发现热点数据自动预热
数据一致性保证
Cache Aside模式(旁路缓存)
应用程序负责维护缓存和数据库的一致性。
读操作:
- 先读缓存,命中则返回
- 缓存不命中,查询数据库
- 将数据写入缓存并返回
写操作:
- 先更新数据库
- 删除缓存(让下次读取时重新加载)
为什么是删除而不是更新缓存?
- 更新缓存可能存在并发问题
- 复杂查询的缓存更新成本高
- 删除缓存更简单可靠
延时双删策略
解决读写并发导致的数据不一致问题。
实现步骤:
1 | public void updateData(String key, Object data) { |
延时时间设置:通常为主从同步时间 + 读数据库时间 + 几十毫秒
基于消息队列的最终一致性
使用消息队列异步处理缓存更新,保证最终一致性。
实现流程:
- 更新数据库,发送消息到队列
- 消息消费者删除相关缓存
- 消费失败时重试,保证最终一致性
1 | // 发送缓存删除消息 |
分布式事务方案
对于强一致性要求高的场景,可以使用分布式事务。
2PC(两阶段提交):
- 准备阶段:协调者询问参与者是否准备好
- 提交阶段:所有参与者都准备好则提交,否则回滚
TCC(Try-Confirm-Cancel):
- Try:尝试执行,预留资源
- Confirm:确认提交
- Cancel:取消执行,释放资源
分布式锁实现
基于SET命令的分布式锁
1 | public class RedisDistributedLock { |
使用方式:
1 | String lockKey = "lock:user:1001"; |
Redlock算法
为了解决单点故障问题,Redis官方提出了Redlock算法。
算法步骤:
- 获取当前时间戳
- 依次向N个Redis实例申请锁
- 如果在大多数实例(N/2+1)上获取锁成功,且总耗时小于锁超时时间,则认为获取锁成功
- 锁的有效时间 = 初始有效时间 - 获取锁消耗的时间
- 释放锁时,向所有Redis实例发送释放命令
1 | public class Redlock { |
锁的问题与优化
锁超时问题: 业务执行时间超过锁超时时间,锁自动释放,可能导致并发问题。
解决方案:
- 看门狗机制:定时续期锁的过期时间
1 | public class WatchDog { |
- 合理评估业务执行时间:设置足够的锁超时时间
锁竞争激烈问题: 大量线程竞争同一把锁,导致性能下降。
解决方案:
- 分段锁:将资源分段,减少锁竞争
- 队列锁:使用List实现公平锁
- 自旋锁优化:适当的退避算法
悲观锁与乐观锁
悲观锁
假设会发生并发冲突,在操作数据前先获取锁。
Redis实现:
1 | // 使用Redis分布式锁实现悲观锁 |
乐观锁
假设不会发生冲突,在更新时检查数据是否被修改。
基于版本号的乐观锁:
1 | public boolean updateWithOptimisticLock(String userId, int amount, int expectedVersion) { |
基于CAS的乐观锁:
1 | public void updateWithCAS(String key, Function<String, String> updater) { |
悲观锁 vs 乐观锁选择
悲观锁适用场景:
- 写操作频繁,冲突概率高
- 业务逻辑复杂,重试成本高
- 对数据一致性要求严格
乐观锁适用场景:
- 读多写少,冲突概率低
- 业务逻辑简单,重试成本低
- 对性能要求高
高可用架构
主从复制
Master负责写操作,Slave负责读操作,通过复制实现数据同步。
复制原理:
- Slave向Master发送PSYNC命令
- Master执行BGSAVE生成RDB文件
- Master将RDB文件发送给Slave
- Slave载入RDB文件
- Master将缓冲区的写命令发送给Slave
- 后续写命令实时同步
部分重同步: 网络断连后,Slave只需要同步断连期间的命令,而不是完整重同步。
哨兵模式(Sentinel)
哨兵负责监控Master状态,在Master故障时自动进行故障转移。
哨兵职责:
- 监控:定期ping Master和Slave
- 通知:故障时通知管理员和客户端
- 故障转移:自动将Slave提升为新Master
- 配置管理:为客户端提供服务发现
故障转移流程:
- 哨兵发现Master下线(主观下线)
- 多个哨兵确认Master下线(客观下线)
- 选举领导哨兵执行故障转移
- 选择合适的Slave作为新Master
- 修改其他Slave的配置指向新Master
- 通知客户端Master地址变更
集群模式(Cluster)
Redis Cluster通过分片实现横向扩展和高可用。
分片算法: 使用CRC16算法计算key的哈希值,然后对16384取模得到槽位号。
节点通信: 使用Gossip协议在节点间交换状态信息,包括节点上线/下线、槽位分配等。
故障转移: 当Master节点故障时,其Slave自动提升为新Master,过程对客户端透明。
数据迁移: 集群扩容时,需要将部分槽位从现有节点迁移到新节点。
性能监控与优化
慢查询日志
Redis提供慢查询日志功能,记录执行时间超过阈值的命令。
配置参数:
1 | slowlog-log-slower-than 10000 # 超过10毫秒记录 |
查看慢查询:
1 | SLOWLOG GET 10 # 获取最近10条慢查询 |
内存分析
使用MEMORY命令分析内存使用情况。
1 | MEMORY USAGE key # 查看key占用内存 |
性能优化建议
避免大key:
- 单个key的value不要超过10KB
- 集合类型元素数量控制在合理范围
- 使用SCAN代替KEYS命令
合理使用数据结构:
- 小对象使用Hash而不是多个String
- 合理设置ziplist等压缩结构的阈值
- 使用位图(bitmap)存储布尔类型大数据集
网络优化:
- 使用Pipeline批量操作
- 合理设置客户端连接池
- 启用TCP_NODELAY选项
持久化优化:
- 根据业务需求选择RDB或AOF
- 合理配置自动保存条件
- 在从节点上进行持久化操作
这些详细的技术点涵盖了Redis的核心概念、常见问题解决方案和实际应用场景,是Redis技术面试的重要考查内容。掌握这些知识点并能结合实际项目经验进行说明,将大大提高面试通过率。
内存管理与淘汰机制
内存淘汰策略详解
当Redis内存使用达到maxmemory限制时,会根据配置的策略淘汰数据。
8种淘汰策略:
1 | # 针对所有key |
LRU vs LFU 实现细节: Redis的LRU并非严格的LRU,而是近似LRU算法:
- 每个key都有24位的时钟字段记录访问时间
- 淘汰时随机采样5个key(可配置),选择时钟值最小的
LFU算法维护访问频率:
- 高16位存储上次访问时间
- 低8位存储访问频率计数器
- 计数器采用概率性递增,避免频率无限增长
内存碎片问题
产生原因:
- 频繁的数据更新导致内存分配/释放
- Redis使用jemalloc内存分配器,存在内存对齐
- 删除大key后留下内存空洞
检测方法:
1 | INFO memory |
解决方案:
- 内存整理(Redis 4.0+):
1 | CONFIG SET activedefrag yes # 开启自动整理 |
- 重启Redis:最彻底但影响服务可用性
- 优化数据结构:减少小对象,使用Hash存储相关数据
过期策略与删除机制
三种过期删除策略
定时删除:设置过期时间时创建定时器,到期立即删除
- 优点:及时释放内存
- 缺点:消耗CPU资源创建和管理定时器
惰性删除:访问key时检查是否过期,过期则删除
- 优点:CPU友好,只在必要时删除
- 缺点:内存不友好,过期key可能长期占用内存
定期删除:定期随机检查部分key,删除过期的
- Redis的实际策略,平衡CPU和内存使用
Redis过期删除实现
1 | // 简化的过期删除逻辑 |
数据结构底层实现深度解析
压缩列表(ZipList)演进
Redis 7.0用ListPack替代了ZipList,解决了级联更新问题。
ZipList问题:
1 | // ZipList结构导致的级联更新 |
ListPack优势:
- 每个元素独立编码,避免级联更新
- 支持从尾部遍历,提高某些操作效率
字典扩容与rehash
Redis字典使用增量式rehash避免阻塞:
1 | // 渐进式rehash实现 |
网络模型与性能优化
Redis 6.0 多线程I/O
多线程只用于网络I/O,命令执行仍是单线程:
1 | // 多线程I/O处理流程 |
客户端连接管理
连接池配置优化:
1 | // Jedis连接池配置 |
Pipeline优化:
1 | // Pipeline批量操作 |
高级数据类型与应用
HyperLogLog
用于基数统计,占用内存固定(12KB),误差率0.81%。
实现原理:
- 基于概率算法,通过观察随机数的最大前导零个数估算基数
- 使用调和平均数减少误差
- 适用于UV统计等场景
1 | # 网站UV统计 |
布隆过滤器(Redis Module)
1 | # Redis布隆过滤器模块 |
Geo地理位置
基于Sorted Set实现,使用GeoHash算法。
1 | # 添加地理位置 |
安全性问题与防护
常见安全漏洞
命令注入:
1 | // 错误做法:直接拼接用户输入 |
未授权访问:
1 | # redis.conf安全配置 |
危险命令禁用:
1 | # 重命名危险命令 |
ACL访问控制(Redis 6.0+)
1 | # 创建用户 |
监控与运维
关键监控指标
性能指标:
1 | # 通过INFO命令获取 |
内存指标:
1 | INFO memory |
持久化指标:
1 | INFO persistence |
故障排查常用命令
1 | # 客户端连接信息 |
特殊应用场景
分布式限流
固定窗口限流:
1 | public class RedisRateLimiter { |
滑动窗口限流:
1 | public boolean slidingWindowLimit(String key, int limit, int window) { |
分布式Session
1 | @Component |
消息队列实现
基于List的简单队列:
1 | public class RedisQueue { |
基于Stream的消息队列(Redis 5.0+):
1 | # 生产消息 |
延时队列
1 | public class RedisDelayQueue { |
性能调优技巧
内存优化
使用Hash存储小对象:
1 | // 不推荐:为每个用户字段单独设置key |
合理设置过期时间:
1 | // 避免同时过期导致雪崩 |
网络优化
批量操作优化:
1 | // 使用Pipeline |
CPU优化
Lua脚本减少网络往返:
1 | -- 原子性增加库存 |
你好,我很乐意为你详细讲解 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
作为键,name
、age
、city
作为字段),商品信息等。
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 单机内存容量和并发量的瓶颈问题。它将数据分散到多个节点上,每个节点只负责存储部分数据。
- 核心功能:
- 数据分片: 自动将数据分布在多个节点上。
- 高可用性: 即使部分节点宕机,集群也能继续正常工作。