前置知识: Java

Java与Redis

00:00
5 min Intermediate 2026/6/14

Redis缓存与数据结构

概述

Redis 是一个基于内存的键值存储系统,支持多种数据结构(字符串、哈希、列表、集合、有序集合等),读写速度极快,单机可达每秒十万次操作。在 Java 应用中,Redis 主要用于缓存、会话管理、排行榜、消息队列等场景。

Java 生态中操作 Redis 的方式主要有三种:Jedis(传统同步客户端)、Lettuce(现代异步客户端)、Spring Data Redis(Spring 生态的抽象层)。对于新项目,推荐使用 Spring Data Redis + Lettuce 的组合,既享受 Spring 的便捷抽象,又获得 Lettuce 的异步性能。

基础概念

Redis 数据结构

Redis 不是简单的 key-value 存储,它支持多种数据结构,每种结构适合不同的场景:

  • String:最基础的类型,可以存字符串、数字、甚至二进制数据(如图片)
  • Hash:键值对集合,类似 Java 的 HashMap,适合存储对象
  • List:有序列表,支持从两端插入和弹出,适合消息队列
  • Set:无序集合,元素唯一,适合去重和交集运算
  • Sorted Set:有序集合,每个元素关联一个分数,适合排行榜

连接方式

Redis 支持单机连接、哨兵模式(Sentinel)和集群模式(Cluster)。开发环境通常用单机,生产环境推荐哨兵或集群以保证高可用。

缓存策略

使用 Redis 做缓存时,需要考虑缓存策略:缓存何时失效、缓存和数据库如何保持一致、缓存穿透和雪崩如何防止。这些不是 Redis 本身的功能,而是使用 Redis 时必须面对的架构问题。

快速上手

添加依赖

Maven 项目中添加 Spring Data Redis 依赖:

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

Gradle 项目中:

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

配置连接

在 application.yml 中配置 Redis 连接信息:

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: your_password
      database: 0
      lettuce:
        pool:
          max-active: 8 # 连接池最大连接数
          max-idle: 8 # 连接池最大空闲连接数
          min-idle: 2 # 连接池最小空闲连接数

最简示例

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 存入字符串
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    // 读取字符串
    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
}

详细用法

1. String 操作

String 是最常用的类型,适合缓存简单值:

@Autowired
private StringRedisTemplate redisTemplate;

// 基本存取
redisTemplate.opsForValue().set("user:name", "Alice");
String name = redisTemplate.opsForValue().get("user:name");

// 设置过期时间(60秒后自动删除)
redisTemplate.opsForValue().set("verify:code", "123456", Duration.ofSeconds(60));

// 仅当 key 不存在时设置(防止覆盖)
Boolean created = redisTemplate.opsForValue().setIfAbsent("lock:order:1001", "locked", Duration.ofSeconds(30));

// 数值递增(适合计数器)
redisTemplate.opsForValue().set("page:views", "0");
redisTemplate.opsForValue().increment("page:views");     // 变为 1
redisTemplate.opsForValue().increment("page:views", 5);  // 变为 6

2. Hash 操作

Hash 适合存储对象,每个字段可以独立读写:

// 存储用户信息
redisTemplate.opsForHash().put("user:1001", "name", "Alice");
redisTemplate.opsForHash().put("user:1001", "age", "25");
redisTemplate.opsForHash().put("user:1001", "email", "alice@example.com");

// 读取单个字段
String email = (String) redisTemplate.opsForHash().get("user:1001", "email");

// 读取所有字段
Map<Object, Object> user = redisTemplate.opsForHash().entries("user:1001");

// 只更新某个字段(不影响其他字段)
redisTemplate.opsForHash().put("user:1001", "age", "26");

// 删除某个字段
redisTemplate.opsForHash().delete("user:1001", "email");

3. List 操作

List 适合实现队列或最新列表:

// 从左侧推入(最新消息在前)
redisTemplate.opsForList().leftPush("notifications:user:1001", "你有新消息");
redisTemplate.opsForList().leftPush("notifications:user:1001", "系统升级通知");

// 获取列表范围(0到-1表示全部)
List<String> notifications = redisTemplate.opsForList().range("notifications:user:1001", 0, -1);

// 从右侧弹出(实现先进先出队列)
String oldest = redisTemplate.opsForList().rightPop("notifications:user:1001");

// 获取列表长度
Long count = redisTemplate.opsForList().size("notifications:user:1001");

4. Set 操作

Set 适合去重和集合运算:

// 添加元素
redisTemplate.opsForSet().add("tags:article:1", "Java", "Redis", "Spring");
redisTemplate.opsForSet().add("tags:article:2", "Java", "MySQL", "Spring");

// 获取所有元素
Set<String> tags = redisTemplate.opsForSet().members("tags:article:1");

// 求交集(两篇文章的共同标签)
Set<String> common = redisTemplate.opsForSet().intersect("tags:article:1", "tags:article:2");

// 判断元素是否存在
Boolean exists = redisTemplate.opsForSet().isMember("tags:article:1", "Java");

// 随机弹出一个元素(适合抽奖场景)
String winner = redisTemplate.opsForSet().pop("lottery:users");

5. Sorted Set 操作

Sorted Set 适合排行榜:

// 添加元素(带分数)
redisTemplate.opsForZSet().add("leaderboard", "Alice", 95.0);
redisTemplate.opsForZSet().add("leaderboard", "Bob", 88.5);
redisTemplate.opsForZSet().add("leaderboard", "Charlie", 92.0);

// 按分数从高到低获取排名(0表示最高分)
Set<String> top3 = redisTemplate.opsForZSet().reverseRange("leaderboard", 0, 2);

// 获取某个人的排名(从0开始)
Long rank = redisTemplate.opsForZSet().reverseRank("leaderboard", "Alice");

// 获取某个人的分数
Double score = redisTemplate.opsForZSet().score("leaderboard", "Alice");

// 增加分数
redisTemplate.opsForZSet().incrementScore("leaderboard", "Bob", 5.0);

6. 使用 @Cacheable 注解

Spring 提供了声明式缓存注解,无需手动操作 Redis:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 查询时先查缓存,缓存未命中再查数据库并写入缓存
    @Cacheable(value = "users", key = "#id")
    public User getUser(Long id) {
        // 这个方法只在缓存未命中时执行
        System.out.println("从数据库查询用户: " + id);
        return userRepository.findById(id).orElse(null);
    }

    // 更新数据时同时更新缓存
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        userRepository.save(user);
        return user;
    }

    // 删除数据时同时清除缓存
    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }

    // 清除该缓存下的所有条目
    @CacheEvict(value = "users", allEntries = true)
    public void clearAllUserCache() {
        // 不需要实际操作,注解会自动清除缓存
    }
}

使用缓存注解前,需要在启动类上添加 @EnableCaching:

@EnableCaching
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

7. 配置缓存序列化

默认情况下 Spring Cache 使用 Java 序列化存储对象,可读性差。建议配置 JSON 序列化:

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30)) // 默认过期时间30分钟
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

常见场景

场景一:验证码存储

验证码需要设置短时间过期,Redis 的 TTL 功能天然适合:

@Service
public class VerifyCodeService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 生成并存储验证码,5分钟有效
    public void sendCode(String phone) {
        String code = String.valueOf((int)((Math.random() * 9 + 1) * 100000));
        redisTemplate.opsForValue().set("code:" + phone, code, Duration.ofMinutes(5));
        // 发送短信...
        System.out.println("验证码已发送: " + code);
    }

    // 验证验证码
    public boolean verifyCode(String phone, String inputCode) {
        String key = "code:" + phone;
        String savedCode = redisTemplate.opsForValue().get(key);
        if (savedCode != null && savedCode.equals(inputCode)) {
            redisTemplate.delete(key); // 验证成功后删除,防止重复使用
            return true;
        }
        return false;
    }
}

场景二:分布式锁

在分布式系统中,实例可能同时操作同一资源,需要用 Redis 实现分布式锁:

@Service
public class OrderService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean createOrder(String orderId) {
        String lockKey = "lock:order:" + orderId;
        // 尝试获取锁,30秒后自动释放
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "locked", Duration.ofSeconds(30));

        if (Boolean.TRUE.equals(locked)) {
            try {
                // 执行业务逻辑
                doCreateOrder(orderId);
                return true;
            } finally {
                // 释放锁
                redisTemplate.delete(lockKey);
            }
        }
        return false; // 获取锁失败,说明其他实例正在处理
    }

    private void doCreateOrder(String orderId) {
        // 实际的订单创建逻辑
    }
}

场景三:限流

使用 Redis 实现滑动窗口

@Service
public class RateLimitService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 检查是否超过限制(每分钟最多60次请求)
    public boolean isAllowed(String userId) {
        String key = "rate:" + userId;
        Long count = redisTemplate.opsForValue().increment(key);
        if (count != null && count == 1) {
            // 第一次请求,设置过期时间
            redisTemplate.expire(key, Duration.ofMinutes(1));
        }
        return count != null && count <= 60;
    }
}

注意事项与常见错误

缓存穿透

查询一个数据库中不存在的数据,每次请求都会穿透到数据库解决方案缓存

@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
    User user = userRepository.findById(id).orElse(null);
    if (user == null) {
        // 缓存空对象,设置较短的过期时间
        redisTemplate.opsForValue().set("users::" + id, "NULL", Duration.ofMinutes(5));
    }
    return user;
}

缓存雪崩

大量缓存同时过期,导致请求全部打到数据库解决方案是给过期时间随机偏移:

// 不要让所有缓存同时过期
long baseTtl = 30; // 基础过期时间30分钟
long randomTtl = baseTtl + (long)(Math.random() * 10); // 加0-10分钟随机偏移
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(randomTtl));

序列化问题

使用 Spring Data Redis 存储对象时,对象必须实现 Serializable 接口(如果使用 Java 默认序列化)。建议配置 JSON 序列化来避免这个问题,同时提可读性。

连接泄漏

使用 Jedis 时,忘记归还连接会导致连接泄漏。Lettuce 基于 Netty 的异步模型,不存在这个问题,这也是推荐 Lettuce 的原因之一。

进阶用法

Redis Stream 消息队列

Redis 5.0 引入了 Stream 数据类型,可以作为轻量级消息队列使用:

// 生产消息
MapRecord<String, String, String> record = StreamRecords.newRecord()
    .ofMap(Map.of("orderId", "1001", "action", "created"))
    .withStreamKey("order-events");

RecordId id = redisTemplate.opsForStream().add(record);

// 消费消息
List<MapRecord<String, String, String>> messages = redisTemplate.opsForStream()
    .read(StreamOffset.fromLast("order-events"));

Redis Pipeline 批量操作

当需要执行大量 Redis 命令时,使用 Pipeline 可以减少网络往返:

redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    for (int i = 0; i < 1000; i++) {
        connection.stringCommands().set(
            ("key:" + i).getBytes(),
            ("value:" + i).getBytes()
        );
    }
    return null; // Pipeline 模式下返回值由框架收集
});

Redis 事务

Redis 支持事务保证一组命令顺序执行不被打断:

redisTemplate.execute(new SessionCallback<Object>() {
    @Override
    public Object execute(RedisOperations operations) throws DataAccessException {
        operations.multi(); // 开启事务
        operations.opsForValue().set("account:A", "900");
        operations.opsForValue().set("account:B", "1100");
        return operations.exec(); // 提交事务
    }
});

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式