Spring Boot 项目集成 Redis

Redis是我们Java开发中,使用频次非常高的一个nosql数据库,数据以key-value键值对的形式存储在内存中。

redis的常用使用场景,可以做缓存,分布式锁,自增序列等

使用redis的方式和我们使用数据库的方式差不多,首先我们要在自己的本机电脑或者服务器上安装一个redis的服务器,通过我们的java客户端在程序中进行集成,然后通过客户端完成对redis的增删改查操作。redis的Java客户端类型还是很多的,常见的有jedis, redission,lettuce等,所以我们在集成的时候,我们可以选择直接集成这些原生客户端。但是在springBoot中更常见的方式是集成spring-data-redis,这是spring提供的一个专门用来操作redis的项目,封装了对redis的常用操作,里边主要封装了jedis和lettuce两个客户端。相当于是在他们的基础上加了一层门面。

一、基础配置

1. 添加依赖

XML 复制代码
<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Starter Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- 连接池(推荐) -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    
    <!-- Jackson JSON序列化 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    
    <!-- Redisson(分布式锁等高级功能) -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.23.2</version>
    </dependency>
</dependencies>

2. 基础配置类

bash 复制代码
# application.yml
spring:
  redis:
    # 单节点配置
    host: localhost
    port: 6379
    password: ${REDIS_PASSWORD:}  # 密码,没有可不填
    database: 0                   # 数据库索引 (0-15)
    timeout: 2000ms              # 连接超时时间
    connect-timeout: 2000ms      # 连接超时
    
    # 连接池配置
    lettuce:
      pool:
        max-active: 20           # 最大连接数
        max-idle: 10             # 最大空闲连接
        min-idle: 5              # 最小空闲连接
        max-wait: -1ms           # 获取连接最大等待时间(-1表示无限等待)
    # 或者使用jedis
    # jedis:
    #   pool:
    #     max-active: 20
    #     max-idle: 10
    #     min-idle: 5
    
    # 集群配置(如果使用集群)
    # cluster:
    #   nodes: 
    #     - 192.168.1.101:6379
    #     - 192.168.1.102:6379
    #     - 192.168.1.103:6379
    #   max-redirects: 3         # 最大重定向次数
    
    # 哨兵配置(如果使用哨兵)
    # sentinel:
    #   master: mymaster
    #   nodes:
    #     - 192.168.1.101:26379
    #     - 192.168.1.102:26379
    #     - 192.168.1.103:26379

3. Redis配置类

java 复制代码
package com.example.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    
    /**
     * 配置自定义RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 设置序列化方式
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.PROPERTY
        );
        jacksonSerializer.setObjectMapper(om);
        
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        
        // key 和 hashKey 使用 String 序列化
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        
        // value 和 hashValue 使用 JSON 序列化
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
    
    /**
     * 配置String专用的RedisTemplate
     */
    @Bean
    public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

二、基础使用示例

1. 基础服务类

java 复制代码
package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class RedisService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * ============ 通用操作 ============
     */
    
    /**
     * 设置缓存过期时间
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            log.error("设置过期时间失败 key: {}", key, e);
            return false;
        }
    }
    
    /**
     * 获取过期时间
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    
    /**
     * 判断key是否存在
     */
    public boolean hasKey(String key) {
        try {
            return Boolean.TRUE.equals(redisTemplate.hasKey(key));
        } catch (Exception e) {
            log.error("判断key是否存在失败 key: {}", key, e);
            return false;
        }
    }
    
    /**
     * 删除缓存
     */
    @SuppressWarnings("unchecked")
    public void delete(String... keys) {
        if (keys != null && keys.length > 0) {
            if (keys.length == 1) {
                redisTemplate.delete(keys[0]);
            } else {
                redisTemplate.delete(Arrays.asList(keys));
            }
        }
    }
    
    /**
     * 批量删除(使用模式匹配)
     */
    public void deletePattern(String pattern) {
        Set<String> keys = redisTemplate.keys(pattern);
        if (!CollectionUtils.isEmpty(keys)) {
            redisTemplate.delete(keys);
        }
    }
    
    /**
     * ============ String 操作 ============
     */
    
    public void set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
        } catch (Exception e) {
            log.error("设置缓存失败 key: {}, value: {}", key, value, e);
        }
    }
    
    public void set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
        } catch (Exception e) {
            log.error("设置缓存失败 key: {}, value: {}, time: {}", key, value, time, e);
        }
    }
    
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    
    public <T> T get(String key, Class<T> clazz) {
        Object value = get(key);
        return value == null ? null : clazz.cast(value);
    }
    
    /**
     * 递增
     */
    public long increment(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    
    /**
     * 递减
     */
    public long decrement(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().decrement(key, delta);
    }
    
    /**
     * ============ Hash 操作 ============
     */
    
    public void hashSet(String key, String hashKey, Object value) {
        try {
            redisTemplate.opsForHash().put(key, hashKey, value);
        } catch (Exception e) {
            log.error("设置hash缓存失败 key: {}, hashKey: {}, value: {}", 
                     key, hashKey, value, e);
        }
    }
    
    public Object hashGet(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }
    
    public Map<Object, Object> hashGetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    
    public void hashDelete(String key, Object... hashKeys) {
        redisTemplate.opsForHash().delete(key, hashKeys);
    }
    
    public boolean hashHasKey(String key, String hashKey) {
        return redisTemplate.opsForHash().hasKey(key, hashKey);
    }
    
    /**
     * ============ List 操作 ============
     */
    
    public void listRightPush(String key, Object value) {
        redisTemplate.opsForList().rightPush(key, value);
    }
    
    public void listLeftPush(String key, Object value) {
        redisTemplate.opsForList().leftPush(key, value);
    }
    
    public Object listRightPop(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }
    
    public Object listLeftPop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }
    
    public List<Object> listRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }
    
    public long listSize(String key) {
        Long size = redisTemplate.opsForList().size(key);
        return size == null ? 0 : size;
    }
    
    /**
     * ============ Set 操作 ============
     */
    
    public void setAdd(String key, Object... values) {
        redisTemplate.opsForSet().add(key, values);
    }
    
    public Set<Object> setMembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }
    
    public boolean setIsMember(String key, Object value) {
        return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, value));
    }
    
    public long setSize(String key) {
        Long size = redisTemplate.opsForSet().size(key);
        return size == null ? 0 : size;
    }
    
    /**
     * ============ ZSet 操作 ============
     */
    
    public boolean zSetAdd(String key, Object value, double score) {
        return Boolean.TRUE.equals(redisTemplate.opsForZSet().add(key, value, score));
    }
    
    public Set<Object> zSetRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }
    
    public Set<Object> zSetReverseRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().reverseRange(key, start, end);
    }
    
    /**
     * ============ 分布式锁 ============
     */
    
    private static final String LOCK_PREFIX = "lock:";
    private static final long DEFAULT_EXPIRE = 30L;
    private static final long DEFAULT_WAIT = 10L;
    
    /**
     * 获取分布式锁(简单实现)
     */
    public boolean tryLock(String key, String value, long expireSeconds) {
        String lockKey = LOCK_PREFIX + key;
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
    
    /**
     * 释放分布式锁
     */
    public boolean unlock(String key, String value) {
        String lockKey = LOCK_PREFIX + key;
        String currentValue = (String) redisTemplate.opsForValue().get(lockKey);
        
        if (currentValue != null && currentValue.equals(value)) {
            redisTemplate.delete(lockKey);
            return true;
        }
        return false;
    }
}

2. 业务场景示例

java 复制代码
package com.example.service.impl;

import com.example.model.User;
import com.example.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
@CacheConfig(cacheNames = "user")  // 统一配置缓存名称
public class UserServiceImpl implements UserService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    
    public UserServiceImpl(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    /**
     * 使用Spring Cache注解方式
     * 自动缓存用户信息,key为 userId,value为 User对象
     */
    @Override
    @Cacheable(key = "#userId", unless = "#result == null")
    public User getUserById(Long userId) {
        log.info("从数据库查询用户: {}", userId);
        // 模拟数据库查询
        return findUserFromDB(userId);
    }
    
    /**
     * 更新用户信息并更新缓存
     */
    @Override
    @CachePut(key = "#user.id")
    public User updateUser(User user) {
        log.info("更新用户信息: {}", user.getId());
        // 模拟数据库更新
        return updateUserInDB(user);
    }
    
    /**
     * 删除用户并清除缓存
     */
    @Override
    @CacheEvict(key = "#userId")
    public void deleteUser(Long userId) {
        log.info("删除用户: {}", userId);
        // 模拟数据库删除
        deleteUserFromDB(userId);
    }
    
    /**
     * 清除所有用户缓存
     */
    @Override
    @CacheEvict(allEntries = true)
    public void clearAllCache() {
        log.info("清除所有用户缓存");
    }
    
    /**
     * 手动缓存操作示例
     */
    public User getUserWithManualCache(Long userId) {
        String cacheKey = "user:" + userId;
        
        // 1. 先从缓存获取
        User user = (User) redisTemplate.opsForValue().get(cacheKey);
        
        if (user != null) {
            log.info("从缓存获取用户: {}", userId);
            return user;
        }
        
        // 2. 缓存未命中,查询数据库
        log.info("缓存未命中,从数据库查询用户: {}", userId);
        user = findUserFromDB(userId);
        
        if (user != null) {
            // 3. 写入缓存,设置过期时间
            redisTemplate.opsForValue().set(
                cacheKey, 
                user, 
                30, // 30分钟过期
                TimeUnit.MINUTES
            );
        }
        
        return user;
    }
    
    /**
     * 防止缓存穿透示例
     */
    public User getUserWithPenetrationProtection(Long userId) {
        String cacheKey = "user:" + userId;
        String nullKey = "null:user:" + userId;
        
        // 1. 检查是否在空值缓存中
        if (Boolean.TRUE.equals(redisTemplate.hasKey(nullKey))) {
            log.info("命中空值缓存,直接返回null");
            return null;
        }
        
        // 2. 从缓存获取
        User user = (User) redisTemplate.opsForValue().get(cacheKey);
        if (user != null) {
            return user;
        }
        
        // 3. 缓存未命中,查询数据库
        user = findUserFromDB(userId);
        
        if (user == null) {
            // 数据库也没有,缓存空值防止穿透
            redisTemplate.opsForValue().set(
                nullKey, 
                "", 
                5, // 较短时间,比如5分钟
                TimeUnit.MINUTES
            );
            return null;
        }
        
        // 4. 缓存正常数据
        redisTemplate.opsForValue().set(
            cacheKey, 
            user, 
            30, 
            TimeUnit.MINUTES
        );
        
        return user;
    }
    
    /**
     * 使用Redis实现分布式Session
     */
    public void handleUserSession(String sessionId, User user) {
        String sessionKey = "session:" + sessionId;
        
        // 存储session
        redisTemplate.opsForValue().set(
            sessionKey, 
            user, 
            30, // 30分钟过期
            TimeUnit.MINUTES
        );
        
        // 同时存储用户ID到session的映射
        redisTemplate.opsForValue().set(
            "user_session:" + user.getId(), 
            sessionId, 
            30, 
            TimeUnit.MINUTES
        );
    }
    
    /**
     * 使用Redis实现发布订阅
     */
    public void publishMessage(String channel, String message) {
        redisTemplate.convertAndSend(channel, message);
    }
    
    /**
     * 模拟数据库操作
     */
    private User findUserFromDB(Long userId) {
        // 模拟数据库查询
        try {
            Thread.sleep(100); // 模拟查询耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return userId > 0 ? new User(userId, "用户" + userId) : null;
    }
    
    private User updateUserInDB(User user) {
        // 模拟数据库更新
        return user;
    }
    
    private void deleteUserFromDB(Long userId) {
        // 模拟数据库删除
    }
}

// 用户实体类
package com.example.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private Long id;
    private String username;
    private String email;
    
    // 需要实现Serializable接口才能被Redis序列化
}

3.序列化

redis的序列化是我们把对象存入到redis中到底以什么方式存储的。redis的序列化方式是默认的序列化方式。RedisTemplate这个类的泛型是<String,Object>,也就是他是支持写入Object对象的,那么这个对象可以是二进制数据,可以是xml也可以是json。一般情况下会使用 JSON 方式序列化成字符串,存储到 Redis 中 。

Redis本身提供了以下几种序列化的方式:

  • GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化
  • Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的
  • JacksonJsonRedisSerializer: 序列化object对象为json字符串
  • JdkSerializationRedisSerializer: 序列化java对象
  • StringRedisSerializer: 简单的字符串序列化

如果我们存储的是String类型,默认使用的是StringRedisSerializer 这种序列化方式。如果我们存储的是对象,默认使用的是 JdkSerializationRedisSerializer,也就是Jdk的序列化方式(通过ObjectOutputStream和ObjectInputStream实现,缺点是我们无法直观看到存储的对象内容)。

java 复制代码
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(factory);
        
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        
        redisTemplate.setKeySerializer(stringRedisSerializer); // key的序列化类型

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 方法过期,改为下面代码
//        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
       
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

三、高级特性配置

1. Redisson分布式锁配置

java 复制代码
package com.example.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    
    @Value("${spring.redis.host}")
    private String host;
    
    @Value("${spring.redis.port}")
    private String port;
    
    @Value("${spring.redis.password:}")
    private String password;
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        // 单节点配置
        config.useSingleServer()
              .setAddress(String.format("redis://%s:%s", host, port))
              .setPassword(StringUtils.hasText(password) ? password : null)
              .setDatabase(0)
              .setConnectionPoolSize(20)      // 连接池大小
              .setConnectionMinimumIdleSize(5) // 最小空闲连接数
              .setIdleConnectionTimeout(10000) // 连接空闲超时时间
              .setConnectTimeout(10000)        // 连接超时时间
              .setTimeout(3000);               // 命令等待超时时间
        
        // 集群配置示例
        // config.useClusterServers()
        //       .addNodeAddress("redis://192.168.1.101:6379")
        //       .addNodeAddress("redis://192.168.1.102:6379")
        //       .addNodeAddress("redis://192.168.1.103:6379");
        
        return Redisson.create(config);
    }
}

// Redisson分布式锁使用
package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class DistributedLockService {
    
    private final RedissonClient redissonClient;
    
    public DistributedLockService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
    
    /**
     * 使用Redisson分布式锁
     */
    public boolean doWithLock(String lockKey, Runnable task) {
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,最多等待10秒,锁持有时间30秒
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            
            if (isLocked) {
                try {
                    // 执行任务
                    task.run();
                    return true;
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            } else {
                log.warn("获取锁失败: {}", lockKey);
                return false;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取锁被中断", e);
            return false;
        }
    }
    
    /**
     * 可重入锁示例
     */
    public void processWithReentrantLock(String userId) {
        String lockKey = "user:process:" + userId;
        RLock lock = redissonClient.getLock(lockKey);
        
        lock.lock(30, TimeUnit.SECONDS);
        try {
            // 业务逻辑
            processUserOrder(userId);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    /**
     * 读写锁示例
     */
    public void readWriteLockExample() {
        // 获取读写锁
        var rwLock = redissonClient.getReadWriteLock("my_rw_lock");
        
        // 读锁
        rwLock.readLock().lock();
        try {
            // 读取操作
            readData();
        } finally {
            rwLock.readLock().unlock();
        }
        
        // 写锁
        rwLock.writeLock().lock();
        try {
            // 写入操作
            writeData();
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    
    private void processUserOrder(String userId) {
        // 业务逻辑
    }
    
    private void readData() {
        // 读取数据
    }
    
    private void writeData() {
        // 写入数据
    }
}

2. Redis消息队列

java 复制代码
package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
@Slf4j
public class RedisMessageService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisMessageListenerContainer container;
    
    // 线程池处理消息
    private final ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    public RedisMessageService(RedisTemplate<String, Object> redisTemplate,
                               RedisMessageListenerContainer container) {
        this.redisTemplate = redisTemplate;
        this.container = container;
    }
    
    /**
     * 发布消息
     */
    public void publish(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
        log.info("发布消息到频道 {}: {}", channel, message);
    }
    
    /**
     * 订阅频道
     */
    public void subscribe(String channel, MessageListener listener) {
        container.addMessageListener(listener, new ChannelTopic(channel));
        log.info("订阅频道: {}", channel);
    }
    
    /**
     * 初始化订阅
     */
    @PostConstruct
    public void init() {
        // 订阅用户相关的消息
        subscribe("user:created", new UserCreatedListener());
        subscribe("order:paid", new OrderPaidListener());
    }
    
    /**
     * 用户创建消息监听器
     */
    private class UserCreatedListener implements MessageListener {
        @Override
        public void onMessage(Message message, byte[] pattern) {
            executorService.submit(() -> {
                try {
                    String channel = new String(message.getChannel());
                    String body = new String(message.getBody());
                    log.info("收到用户创建消息: 频道={}, 内容={}", channel, body);
                    
                    // 处理用户创建逻辑
                    handleUserCreated(body);
                } catch (Exception e) {
                    log.error("处理用户创建消息异常", e);
                }
            });
        }
        
        private void handleUserCreated(String message) {
            // 解析消息并处理
            log.info("处理用户创建: {}", message);
        }
    }
    
    /**
     * 订单支付消息监听器
     */
    private class OrderPaidListener implements MessageListener {
        @Override
        public void onMessage(Message message, byte[] pattern) {
            executorService.submit(() -> {
                try {
                    String channel = new String(message.getChannel());
                    String body = new String(message.getBody());
                    log.info("收到订单支付消息: 频道={}, 内容={}", channel, body);
                    
                    // 处理订单支付逻辑
                    handleOrderPaid(body);
                } catch (Exception e) {
                    log.error("处理订单支付消息异常", e);
                }
            });
        }
        
        private void handleOrderPaid(String message) {
            // 解析消息并处理
            log.info("处理订单支付: {}", message);
        }
    }
}

// 消息监听器容器配置
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisListenerConfig {
    
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setTaskExecutor(null); // 使用默认执行器
        container.setErrorHandler(e -> {
            // 错误处理
            System.err.println("Redis监听器错误: " + e.getMessage());
        });
        return container;
    }
}

3. Redis Lua脚本支持

java 复制代码
package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collections;

@Service
@Slf4j
public class RedisScriptService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    private DefaultRedisScript<Long> rateLimitScript;
    private DefaultRedisScript<Boolean> compareAndSetScript;
    
    public RedisScriptService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @PostConstruct
    public void init() {
        // 初始化限流脚本
        rateLimitScript = new DefaultRedisScript<>();
        rateLimitScript.setScriptSource(new ResourceScriptSource(
            new ClassPathResource("lua/rate_limiter.lua")
        ));
        rateLimitScript.setResultType(Long.class);
        
        // 初始化CAS脚本
        compareAndSetScript = new DefaultRedisScript<>();
        compareAndSetScript.setScriptSource(new ResourceScriptSource(
            new ClassPathResource("lua/compare_and_set.lua")
        ));
        compareAndSetScript.setResultType(Boolean.class);
    }
    
    /**
     * 使用Lua脚本实现限流
     */
    public boolean rateLimit(String key, int maxRequests, int windowSeconds) {
        Long result = redisTemplate.execute(
            rateLimitScript,
            Collections.singletonList(key),
            String.valueOf(maxRequests),
            String.valueOf(windowSeconds),
            String.valueOf(System.currentTimeMillis())
        );
        
        return result != null && result == 1L;
    }
    
    /**
     * 使用Lua脚本实现原子性比较并设置
     */
    public boolean compareAndSet(String key, String expected, String newValue) {
        Boolean result = redisTemplate.execute(
            compareAndSetScript,
            Collections.singletonList(key),
            expected,
            newValue
        );
        
        return Boolean.TRUE.equals(result);
    }
}

// resources/lua/rate_limiter.lua
-- KEYS[1]: 限流key
-- ARGV[1]: 最大请求数
-- ARGV[2]: 时间窗口(秒)
-- ARGV[3]: 当前时间戳(毫秒)
local key = KEYS[1]
local maxRequests = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 清除时间窗口之前的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

-- 获取当前请求数
local current = redis.call('ZCARD', key)

if current < maxRequests then
    -- 添加当前请求
    redis.call('ZADD', key, now, now)
    -- 设置过期时间
    redis.call('EXPIRE', key, window)
    return 1
else
    return 0
end

// resources/lua/compare_and_set.lua
-- KEYS[1]: 缓存的key
-- ARGV[1]: 期望的值
-- ARGV[2]: 新值
local current = redis.call('GET', KEYS[1])

if current == ARGV[1] then
    redis.call('SET', KEYS[1], ARGV[2])
    return true
else
    return false
end

4. Redis缓存雪崩/穿透/击穿防护

java 复制代码
package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class CacheProtectionService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    private final Random random = new Random();
    
    // 本地锁,用于防止缓存击穿
    private final ReentrantLock localLock = new ReentrantLock();
    
    public CacheProtectionService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    /**
     * 防止缓存击穿 - 使用互斥锁
     */
    public Object getWithBreakdownProtection(String key, CacheLoader loader) {
        // 1. 从缓存获取
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return value;
        }
        
        // 2. 获取本地锁(防止多个线程同时查询数据库)
        localLock.lock();
        try {
            // 双重检查
            value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                return value;
            }
            
            // 3. 从数据源加载
            value = loader.load();
            
            if (value != null) {
                // 4. 写入缓存
                redisTemplate.opsForValue().set(
                    key, 
                    value, 
                    30 + random.nextInt(30), // 基础30分钟 + 随机0-30分钟
                    TimeUnit.MINUTES
                );
            }
            
            return value;
        } finally {
            localLock.unlock();
        }
    }
    
    /**
     * 防止缓存穿透 - 布隆过滤器(简化版)
     */
    public Object getWithPenetrationProtection(String key, CacheLoader loader) {
        // 1. 检查是否存在(可以使用布隆过滤器)
        String existsKey = "exists:" + key;
        Boolean exists = redisTemplate.opsForValue().get(existsKey);
        
        if (Boolean.FALSE.equals(exists)) {
            log.info("数据不存在,防止缓存穿透: {}", key);
            return null;
        }
        
        // 2. 正常缓存逻辑
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return value;
        }
        
        // 3. 从数据源加载
        value = loader.load();
        
        if (value == null) {
            // 数据不存在,设置标记防止穿透
            redisTemplate.opsForValue().set(
                existsKey, 
                "false", 
                5, // 较短时间
                TimeUnit.MINUTES
            );
        } else {
            // 数据存在,写入缓存
            redisTemplate.opsForValue().set(
                key, 
                value, 
                30 + random.nextInt(30), // 随机过期时间
                TimeUnit.MINUTES
            );
        }
        
        return value;
    }
    
    /**
     * 防止缓存雪崩 - 热点数据永不过期 + 异步刷新
     */
    public void setupHotDataRefresh(String key, CacheLoader loader) {
        // 1. 加载数据
        Object value = loader.load();
        
        if (value != null) {
            // 2. 设置缓存,永不过期
            redisTemplate.opsForValue().set(key, value);
            
            // 3. 异步刷新线程
            new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(60 * 60 * 1000); // 每小时刷新一次
                        
                        Object newValue = loader.load();
                        if (newValue != null) {
                            redisTemplate.opsForValue().set(key, newValue);
                            log.info("热点数据刷新成功: {}", key);
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }).start();
        }
    }
    
    /**
     * 缓存加载器接口
     */
    @FunctionalInterface
    public interface CacheLoader {
        Object load();
    }
}

5. Redis集群/哨兵配置

bash 复制代码
# application-cluster.yml
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.101:6379
        - 192.168.1.102:6379
        - 192.168.1.103:6379
        - 192.168.1.104:6379
        - 192.168.1.105:6379
        - 192.168.1.106:6379
      max-redirects: 3
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
      cluster:
        refresh:
          adaptive: true
          period: 2000

# application-sentinel.yml
spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - 192.168.1.101:26379
        - 192.168.1.102:26379
        - 192.168.1.103:26379
      password: ${REDIS_PASSWORD}
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

四、监控和健康检查

java 复制代码
package com.example.config;

import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisHealthIndicator implements HealthIndicator {
    
    private final RedisConnectionFactory connectionFactory;
    private final MeterRegistry meterRegistry;
    
    public RedisHealthIndicator(RedisConnectionFactory connectionFactory,
                                MeterRegistry meterRegistry) {
        this.connectionFactory = connectionFactory;
        this.meterRegistry = meterRegistry;
    }
    
    @Override
    public Health health() {
        long startTime = System.nanoTime();
        
        try {
            // 执行PING命令检查连接
            String result = connectionFactory.getConnection().ping();
            
            long duration = System.nanoTime() - startTime;
            
            // 记录监控指标
            meterRegistry.timer("redis.health.check.duration")
                .record(duration, TimeUnit.NANOSECONDS);
            
            if ("PONG".equals(result)) {
                return Health.up()
                    .withDetail("response", result)
                    .withDetail("duration_ms", duration / 1_000_000.0)
                    .build();
            } else {
                return Health.down()
                    .withDetail("error", "Unexpected response: " + result)
                    .build();
            }
        } catch (Exception e) {
            long duration = System.nanoTime() - startTime;
            meterRegistry.timer("redis.health.check.duration")
                .record(duration, TimeUnit.NANOSECONDS);
            
            log.error("Redis健康检查失败", e);
            return Health.down(e)
                .withDetail("duration_ms", duration / 1_000_000.0)
                .build();
        }
    }
}

// 监控配置
package com.example.config;

import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.core.instrument.binder.cache.RedisCacheMetrics;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;

import javax.annotation.PostConstruct;

@Configuration
public class MetricsConfig {
    
    private final MeterRegistry meterRegistry;
    private final RedisCacheManager cacheManager;
    
    public MetricsConfig(MeterRegistry meterRegistry, 
                        RedisCacheManager cacheManager) {
        this.meterRegistry = meterRegistry;
        this.cacheManager = cacheManager;
    }
    
    @PostConstruct
    public void initMetrics() {
        // 绑定JVM指标
        new JvmMemoryMetrics().bindTo(meterRegistry);
        new JvmThreadMetrics().bindTo(meterRegistry);
        new ProcessorMetrics().bindTo(meterRegistry);
        
        // 绑定Redis缓存指标
        if (cacheManager.getCache("user") != null) {
            RedisCacheMetrics.monitor(
                meterRegistry, 
                cacheManager.getCache("user"),
                "user_cache"
            );
        }
    }
}

五、最佳实践总结

1. 键设计规范

java 复制代码
public class RedisKeyBuilder {
    
    // 统一前缀
    private static final String PREFIX = "app:";
    
    // 业务模块
    private static final String MODULE_USER = "user:";
    private static final String MODULE_ORDER = "order:";
    private static final String MODULE_PRODUCT = "product:";
    
    // 键类型
    private static final String TYPE_CACHE = "cache:";
    private static final String TYPE_LOCK = "lock:";
    private static final String TYPE_COUNTER = "counter:";
    private static final String TYPE_RATE_LIMIT = "rate:limit:";
    
    public static String userCacheKey(Long userId) {
        return PREFIX + MODULE_USER + TYPE_CACHE + userId;
    }
    
    public static String userSessionKey(String sessionId) {
        return PREFIX + MODULE_USER + "session:" + sessionId;
    }
    
    public static String orderLockKey(String orderNo) {
        return PREFIX + MODULE_ORDER + TYPE_LOCK + orderNo;
    }
    
    public static String productStockKey(Long productId) {
        return PREFIX + MODULE_PRODUCT + "stock:" + productId;
    }
    
    public static String apiRateLimitKey(String apiPath, String userId) {
        return PREFIX + TYPE_RATE_LIMIT + apiPath + ":" + userId;
    }
}

2. 配置优化建议

bash 复制代码
spring:
  redis:
    # 生产环境建议配置
    lettuce:
      pool:
        max-active: 50           # 根据并发调整
        max-idle: 20
        min-idle: 10
        max-wait: 1000ms        # 避免无限等待
      shutdown-timeout: 100ms    # 关闭超时时间
    # 超时配置
    timeout: 1000ms
    connect-timeout: 1000ms

3. 性能优化技巧

java 复制代码
// 1. 使用Pipeline批量操作
public void batchOperations() {
    List<Object> results = redisTemplate.executePipelined(
        (RedisCallback<Object>) connection -> {
            for (int i = 0; i < 100; i++) {
                connection.stringCommands()
                    .set(("key:" + i).getBytes(), ("value:" + i).getBytes());
            }
            return null;
        }
    );
}

// 2. 使用SCAN代替KEYS
public Set<String> scanKeys(String pattern) {
    Set<String> keys = new HashSet<>();
    Cursor<byte[]> cursor = redisTemplate.getConnectionFactory()
        .getConnection()
        .scan(ScanOptions.scanOptions().match(pattern).count(100).build());
    
    while (cursor.hasNext()) {
        keys.add(new String(cursor.next()));
    }
    return keys;
}

// 3. 合理设置过期时间
public void setWithRandomExpire(String key, Object value, long baseExpire) {
    Random random = new Random();
    long randomExpire = baseExpire + random.nextInt((int) baseExpire / 2);
    redisTemplate.opsForValue().set(key, value, randomExpire, TimeUnit.SECONDS);
}

4. 常见问题处理

问题 现象 解决方案
连接泄漏 连接数持续增长 1. 检查连接池配置 2. 确保连接正确关闭 3. 使用连接池监控
内存溢出 Redis内存使用率高 1. 设置合理过期时间 2. 监控大key 3. 使用内存淘汰策略
缓存穿透 大量请求不存在的key 1. 布隆过滤器 2. 空值缓存 3. 接口限流
缓存雪崩 大量key同时过期 1. 设置随机过期时间 2. 热点数据永不过期 3. 降级熔断机制
缓存击穿 热点key失效 1. 互斥锁 2. 永不过期 + 异步更新 3. 逻辑过期

六、生产环境部署建议

  1. 高可用架构

    • 使用 Redis Sentinel 或 Redis Cluster

    • 配置合理的副本数量

    • 设置自动故障转移

  2. 备份策略

    • 定期 RDB 快照

    • AOF 持久化开启

    • 异地备份

  3. 监控告警

    • 监控内存使用率

    • 监控连接数

    • 监控命中率

    • 设置慢查询日志

  4. 安全配置

    • 启用密码认证

    • 限制网络访问

    • 禁用危险命令

    • 定期更新密码

相关推荐
_codemonster2 小时前
java web修改了文件和新建了文件需要注意的问题
java·开发语言·前端
Java天梯之路2 小时前
Spring Boot 钩子全集实战(九):`@PostConstruct` 详解
java·spring boot·后端
松涛和鸣2 小时前
75、 IMX6ULL LM75温度传感器I2C驱动开发
java·linux·数据库·驱动开发·python
独自破碎E2 小时前
BISHI41 【模板】整除分块
java·开发语言
树码小子2 小时前
Mybatis(7)其他查询操作(多表查询)
spring boot·mybatis
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Springboot图书管理系统为例,包含答辩的问题和答案
java·spring boot·后端
鹅是开哥2 小时前
Spring AI Alibaba + DashScope 调用超时彻底解决(SocketTimeoutException / read timeout)
java·人工智能·spring
努力学习的小廉2 小时前
redis学习笔记(三)—— hash数据类型
redis·笔记·学习
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于springboot网络游戏账号租赁以及出售系统为例,包含答辩的问题和答案
java·spring boot·后端