每一年都奔走在自己热爱里 没有人是一座孤岛,总有谁爱着你

岁
万
月
12

Redis详解

学习文章:https://blog.csdn.net/weixin_53391173/article/details/140266317

一、Redis的下载和配置

1.1 安装参考:

https://blog.csdn.net/m0_74825678/article/details/145644691

1.2 添加依赖

spring-boot-starter-data-redis、、redisson和redisson-spring-boot-starter三个依赖的区别:
(1)spring-boot-starter-data-redis

  • 定位:
    Spring 官方提供的 Redis 集成模块,抽象了 Redis 客户端(默认使用 Lettuce),简化基本操作(如缓存、字符串、哈希等)的配置和使用。
  • 优点:
    与 Spring 生态无缝集成:支持自动配置、注解缓存(如 @Cacheable)、Spring Session 等,适合快速开发。
    轻量级:专注于 Redis 核心功能(数据存储、缓存),适合常规业务场景。
    灵活性:支持切换客户端(如 Jedis、Lettuce)。
  • 缺点:
    功能有限:仅提供 Redis 基础操作,缺乏分布式锁、队列等高级功能。
    手动实现复杂逻辑:如分布式锁需自行通过 Lua 脚本或 setnx 实现,易出错。

(2)redisson

  • 定位:
    功能强大的 Redis Java 客户端,提供分布式工具集(如分布式锁、原子计数器、队列)和本地缓存等高级功能,扩展了 Redis 的用途。
  • 优点:
    丰富的分布式功能:开箱即用的分布式锁(RLock)、分布式集合(RMap、RList)、分布式限流器等,适合复杂分布式场景。
    高性能与稳定性:基于 Netty 实现异步 I/O,支持连接池和多种部署模式(单节点、集群、哨兵)6。
    与 Spring Boot 集成:通过 redisson-spring-boot-starter 实现自动配置,可直接注入 RedissonClient。
  • 缺点:
    复杂度较高:若仅需基础功能,引入 Redisson 可能增加学习成本和依赖体积。
    配置灵活性较低:依赖自动配置时,需遵循 Redisson 的配置规范。

(3)redisson-spring-boot-starter

  • 定位:
    专为 Spring Boot 设计的 Starter,自动配置 Redisson 客户端,并与 Spring 生态无缝集成。
  • 适用场景:
    Spring Boot 项目,希望快速集成 Redisson。
    需要直接使用 Spring Boot 的配置文件(如 application.yml)管理 Redisson 配置。
  • 优点:
    开箱即用:自动创建 RedissonClient 和 RedisTemplate,无需手动编写配置类。
    配置简化:通过 spring.redis.* 或 spring.redis.redisson.config 直接配置 Redisson。
    版本统一:Starter 会管理核心库版本(如 redisson-spring-boot-starter 3.23.5 已包含 redisson 3.23.5),无需单独指定。
  • 缺点:
    灵活性较低:若需深度定制 Redisson 配置(如复杂集群模式),可能需要覆盖默认配置。

Spring Boot 项目中的选型建议

  • 优先使用 spring-boot-starter-data-redis:
    场景:仅需基础 Redis 操作(如缓存、字符串/哈希存储)或需要与 Spring 生态(如 @Cacheable、Spring Session)深度集成。
    优点:轻量级、配置简单、与 Spring Boot 无缝兼容。
  • 需要分布式功能时选择 redisson-spring-boot-starter:
    场景:需分布式锁(RLock)、队列(RQueue)、限流器等高级功能,或需要替代默认 Lettuce 客户端以提升性能。
    优点:开箱即用,自动配置 RedissonClient,减少手动编码复杂度。
  • 避免单独使用 redisson 核心库:
    场景:非 Spring Boot 项目或需完全自定义 Redisson 配置(如动态加载配置)。
    缺点:需手动管理连接池和 Bean 注入,增加代码复杂度。

推荐使用组合:基础功能 + 高级功能共存:
同时引入 spring-boot-starter-data-redis 和 redisson-spring-boot-starter,通过 RedisTemplate 处理基础操作,RedissonClient 处理分布式功能。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.5</version>
</dependency>

1.3 配置文件

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123456
      database: 0 # Redis数据库索引(默认为0)
      timeout: 5000 # 连接超时时间

1.4 启动缓存

在启动类添加注解@EnableCaching


1.5 RedisTemplate和RedissonClient

  • RedisTemplate是Spring Data Redis 提供的通用 Redis 操作模板,支持标准 Redis 命令和数据结构。 提供对字符串、哈希、列表、集合等数据结构的通用操作,支持序列化配置和事务管理。
  • RedissonClient是Redisson 提供的分布式 Redis 客户端,专注于分布式场景和高级功能。 提供分布式锁(RLock)、队列(RQueue)、原子计数器(RAtomicLong)等分布式工具,支持异步、流式操作。

image

(1) 使用 RedissonClient 替代 RedisTemplate

  • 可行但需权衡:
    Redisson 的 RBucket、RMap 等接口可以替代 RedisTemplate 的基础操作。
  • 缺点:
    Redisson 的 API 与 Spring Data Redis 不兼容(如不支持 @Cacheable 注解直接绑定)。
    需要手动管理数据结构的序列化逻辑。

(2) 使用 RedisTemplate 替代 RedissonClient

  • 不可行:
    RedisTemplate 缺乏分布式锁、队列等高级功能,需自行实现(如通过 Lua 脚本),复杂度高且易出错。

1.6 本项目配置

  • springboot版本:3.4.3
  • jdk:17
  • 依赖:
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson-spring-boot-starter</artifactId>
                <version>3.23.5</version>
            </dependency>
  • orm框架:mybatis
  • 容器:Undertow
  • 配置文件:
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123456
      database: 0 # Redis数据库索引(默认为0)
      timeout: 5000 # 连接超时时间

二、使用场景

2.1 缓存

Redis 可以作为应用程序的缓存层,减少数据库的读取压力,提高数据访问速度。

2.1.1 场景代码示例

(1)在查询数据时,先从缓存中获取,若为空,再查库,若不为空,直接返回结果:

    @Autowired
    private RedissonClient redissonClient;

    // 缓存前缀(包含业务标识和参数模板)
    private static final String CACHE_KEY_PREFIX = "user:list:page:%d:size:%d";

    @Override
    public PageInfo<SysUser> selectUser(UserDto user) {
        // 1. 生成唯一缓存键
        String cacheKey = String.format(CACHE_KEY_PREFIX, user.getPageIndex(), user.getPageSize());
        // 2. 从 Redis 查询缓存
        RBucket<PageInfo<SysUser>> bucket = redissonClient.getBucket(cacheKey);
        PageInfo<SysUser> cachedPage = bucket.get();
        // 3. 缓存命中直接返回
        if (cachedPage != null) {
            return cachedPage;
        }
        //4. 缓存未命中,查询数据库
        PageHelper.startPage(user.getPageIndex(), user.getPageSize());
        List<SysUser> list = sysUserMapper.selectAll(user);
        PageInfo<SysUser> pageInfo = new PageInfo<>(list);
        // 5. 将结果写入 Redis(设置 TTL 避免长期未更新)
        bucket.set(pageInfo, 30, TimeUnit.MINUTES); // 缓存30分钟
        return pageInfo;
    }

(2)数据有变动时清除缓存:

@Override
    public void updateUser(UserDto user) {
        //更新数据
        SysUser sysUser = new SysUser();
        BeanUtils.copyProperties(user, sysUser);
        sysUser.setUpdateTime(LocalDateTime.now());
        sysUserMapper.updateById(sysUser);

        // 通配符匹配所有分页缓存键(需根据实际场景优化)
        RKeys keys = redissonClient.getKeys();
//        Iterable<String> pageKeys = keys.getKeysByPattern("user:list:page:*");
//        // 将 Iterable 转换为 List<String>
//        List<String> pageKeysList = new ArrayList<>();
//        pageKeys.forEach(pageKeysList::add);
//
//        // 转换为数组并删除
//        if (!pageKeysList.isEmpty()) {
//            keys.delete(pageKeysList.toArray(new String[0]));
//        }

        // 或者直接使用通配符删除
        long deletedCount = keys.deleteByPattern("user:list:page:*");
        log.info("已删除缓存键数量:" + deletedCount);
    }

2.1.2 优化建议

1、缓存键设计优化:

// 添加时间戳版本号,避免全量清除缓存(更精细控制)
String CACHE_KEY_PREFIX = "user:list:v1:page:%d:size:%d";
// 数据更新时只需修改版本号 v1 -> v2

2、批量删除缓存优化:

public void clearPageCache() {
    RMapCache<String, Object> cache = redissonClient.getMapCache("pageCache");
    cache.clear(); // 使用命名空间管理缓存
}

3、防止缓存穿透:

// 空结果也缓存(避免频繁查询不存在的数据)
if (userList.isEmpty()) {
    bucket.set(new PageInfo<>(), 5, TimeUnit.MINUTES); // 短期缓存空结果
}

4、防止缓存击穿:
使用分布式锁解决,轻松应对高并发场景:

    // 缓存前缀(包含业务标识和参数模板)
    private static final String CACHE_KEY_PREFIX = "user:list:page:%d:size:%d";

    //锁键
    private static final String LOCK_KEY_PREFIX = "lock:user:list:page:%d:size:%d";

    @Override
    public PageInfo<SysUser> selectUser(UserDto user) {
        //分布式版本
        // 1. 生成缓存键和锁键
        String cacheKey = String.format(CACHE_KEY_PREFIX, user.getPageIndex(), user.getPageSize());
        String lockKey = String.format(LOCK_KEY_PREFIX, user.getPageIndex(), user.getPageSize());

        // 2. 尝试从缓存读取数据
        RBucket<PageInfo<SysUser>> bucket = redissonClient.getBucket(cacheKey);
        PageInfo<SysUser> cachedPage = bucket.get();
        if (cachedPage != null) {
            return cachedPage; // 缓存命中直接返回
        }

        // 3. 缓存未命中,尝试获取分布式锁
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 3.1 尝试加锁(最多等待2秒,锁超时自动释放时间为10秒)
            boolean isLocked = lock.tryLock(2, 10, TimeUnit.SECONDS);
            if (!isLocked) {
                // 获取锁失败,可重试或直接查询数据库(根据业务容忍度决定)
                return selectPage(user);
            }

            // 3.2 二次检查缓存(防止其他线程已更新)
            cachedPage = bucket.get();
            if (cachedPage != null) {
                return cachedPage;
            }

            // 3.3 查询数据库并写入缓存
            PageInfo<SysUser> pageInfo = selectPage(user);
            bucket.set(pageInfo, 30, TimeUnit.MINUTES); // 缓存30分钟
            return pageInfo;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁失败", e);
        } finally {
            // 3.4 释放锁(只在当前线程持有锁时释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

在分页查询缓存中使用 Redisson 分布式锁的核心目的是解决 缓存击穿(Cache Breakdown)问题。在高并发场景下,如果大量请求同时访问某个 不存在于缓存但存在于数据库 的数据(例如首次查询新分页或缓存刚过期),所有请求会同时穿透缓存直接访问数据库,导致数据库压力骤增甚至崩溃。分布式锁可以确保 同一时刻只有一个线程去重建缓存,其他线程等待锁释放后直接读取缓存,保证数据的一致性,否则多个线程同时更新缓存可能导致脏数据(例如不同线程以不同顺序更新缓存)。

Redisson 分布式锁的优势

  • 自动续期:锁可设置超时时间,Redisson 默认提供看门狗(Watchdog)机制自动续期,避免死锁。
  • 可重入性:同一线程可重复获取锁,避免嵌套调用时死锁。
  • 高性能:基于 Redis 的原子操作实现,性能远高于数据库锁。

tryLock 参数:

  • waitTime=2:最多等待 2 秒获取锁,超时后放弃加锁(避免线程长时间阻塞)。
  • leaseTime=10:锁持有时间 10 秒,超时后自动释放(防止死锁)。
  • 推荐设置:leaseTime 应大于数据库查询耗时(如预估查询最多 5 秒,则设置 10 秒)。

看门狗(Watchdog)机制:
Redisson 的 看门狗(Watchdog)机制 是一种分布式锁的自动续期机制,用于解决因锁持有者执行时间过长导致锁超时释放的问题,从而避免并发冲突和数据不一致。
(1)看门狗机制的核心原理:

  • 自动续期:
    当线程获取分布式锁时,若未显式指定锁的过期时间(leaseTime),Redisson 会默认启用看门狗机制。看门狗会定期(默认每 10 秒)检查锁的状态,并在锁剩余有效期小于 1/3 的默认超时时间(30 秒) 时,自动将锁的过期时间重置为 30 秒。这一过程循环执行,直至锁被主动释放或线程终止。
  • 防死锁设计:
    若线程因宕机或异常未释放锁,看门狗会停止续期,锁将在最后一次续期后的 30 秒 自动过期,避免永久死锁。

(2)触发条件

  • 未指定 leaseTime:
    使用 lock() 或 tryLock() 方法时,若不传递 leaseTime 参数,或显式设置为 -1,看门狗机制自动启用。
  • 显式设置 leaseTime:
    若指定 leaseTime(如 lock(10, TimeUnit.SECONDS)),则看门狗不生效,锁在指定时间后强制释放。

2.2 会话存储

2.2.1 相关概念

(1)用户会话

  • 在实际应用中,用户会话是一个关键的组成部分,用于存储用户的登录状态、个性化设置等信息。将会话数据存储在Redis中可以实现共享会话,使得用户可以在多个应用实例之间共享登录状态。

(2)会话存储

  • 会话存储是一种将用户的登录状态和其他相关信息保存在服务器端的机制。这样,用户在不同的请求之间可以保持登录状态,而不需要每次请求都重新登录。共享会话则是将用户会话数据存储在可共享的存储系统中,使得不同应用实例之间能够访问和共享相同的会话信息。

2.2.2 Tomcat 和 Undertow 容器对比

(1)性能对比

  • 高并发场景
    Undertow 基于非阻塞 I/O 模型(XNIO 事件驱动),在高并发请求下表现更优。
  • 静态资源处理
    Tomcat 在单线程模式下处理静态文件(如 HTML、图片)的吞吐量略高于 Undertow,但差距较小
  • 长连接与 WebSocket
    Undertow 原生支持 HTTP/2 和 WebSocket,默认启用持久连接,适合实时通信场景(如聊天室、高频交易系统)。Tomcat 需手动配置且性能稍逊

(2)资源消耗

  • 内存占用
    Undertow 内存占用更低。空载时,Undertow 约占用 30MB,而 Tomcat 约 120MB。在高并发下,Undertow 的异步模型减少了线程切换开销,进一步降低内存消耗
  • 启动速度
    Undertow 启动时间更短,适合微服务架构和快速迭代场景

(3)功能与生态系统

  • Tomcat 的优势
    成熟稳定:作为 Apache 项目,拥有庞大的社区和丰富的文档,适合企业级应用和需要完整 Java EE 支持的项目。
    管理工具:提供图形化管理界面(如 Tomcat Manager),便于运维监控。
    兼容性:对老旧技术(如 JSP)支持更好
  • Undertow 的灵活性
    模块化设计:可定制性强,支持编程式配置,适合需要高度定制化的场景。
    轻量级:核心代码精简,适合嵌入到应用或独立运行

(4)适用场景

  • 选择 Tomcat 的情况
    需要稳定性和广泛兼容性的传统企业级应用(如银行系统、政府项目)。
    依赖 JSP 或复杂 Java EE 特性的项目。
    对运维友好性要求高,需图形化工具管理的场景。
  • 选择 Undertow 的情况
    高并发 API 服务(如电商秒杀、实时数据流处理)。
    微服务架构或资源受限环境(如容器化部署)。
    需要低延迟和高吞吐量的实时通信场景(如 WebSocket 服务)

(5)Spring Boot 集成与配置

  • Tomcat
    默认容器,无需额外配置,适合快速开发。可通过 application.yml 调整线程池参数(如 max-threads)
  • Undertow
    需排除 Tomcat 依赖并添加 spring-boot-starter-undertow
<dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-undertow</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <!-- 排除 Tomcat 默认会话管理 -->
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-tomcat</artifactId>
                    </exclusion>
                </exclusions>
                <version>3.4.3</version>
            </dependency>

支持优化线程池和缓冲区配置:

server:
  undertow:
    threads:
      io: 4      # I/O 线程数(默认 CPU 核心数 × 2)
      worker: 40 # 工作线程数(默认 io × 8)
    buffer-size: 1024:cite[10]

(6)结论

  • Tomcat 更适合:稳定性优先、传统企业级应用、需完整 Java EE 支持的项目。
  • Undertow 更适合:高并发、低资源消耗、实时通信场景,或追求极致性能的微服务架构。

2.2.3 Redis优势

  • 高性能
    Redis是一款内存数据库,读写速度极快,适用于需要快速访问会话数据的场景。

  • 数据结构支持
    Redis支持丰富的数据结构,可以存储复杂的会话信息,如哈希、列表等。

  • 持久性
    Redis支持持久化,确保即使在系统重启后,用户的会话数据也不会丢失。

在 Spring Boot 3.x 及更高版本中,spring.session.store-type 属性已被弃用或移除

posted @ 2026-03-19 14:11  果咩那塞  阅读(18)  评论(0)    收藏  举报