SpringBoot 整合 Redis 实战——缓存、分布式锁、Session共享

Redis 是 Java 后端开发中最高频使用的中间件之一,缓存、分布式锁、Session 共享是三大最常用的场景。这一篇从配置到实战,一步到位。

一、基础配置

1. 引入依赖

<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>

2. application.yml

spring:
  redis:
    host: localhost
    port: 6379
    password:       # 如果设置了密码
    database: 0     # 默认 0,共 0-15 个库
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 16   # 连接池最大连接数
        max-idle: 8      # 最大空闲连接
        min-idle: 4      # 最小空闲连接
        max-wait: 3000ms # 获取连接最大等待时间

3. RedisTemplate 配置

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // JSON 序列化
        Jackson2JsonRedisSerializer<Object> jacksonSer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LazyValidatorFactory.class, ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSer.setObjectMapper(om);

        // String 序列化(key 用)
        StringRedisSerializer stringSer = new StringRedisSerializer();

        // key 和 hashKey 用 String 序列化
        template.setKeySerializer(stringSer);
        template.setHashKeySerializer(stringSer);
        // value 和 hashValue 用 JSON 序列化
        template.setValueSerializer(jacksonSer);
        template.setHashValueSerializer(jacksonSer);

        template.afterPropertiesSet();
        return template;
    }

    /**
     * 简单操作时,直接用 StringRedisTemplate
     * 省去序列化配置的麻烦
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }
}

注意: RedisTemplate 默认用 JDK 序列化,存进 Redis 是乱码,必须改成 JSON 序列化。

二、场景一:缓存

1. 缓存查询——最简单的写法

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> 
        implements UserService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String CACHE_KEY = "cache:user:";

    @Override
    public User getUserById(Long id) {
        // 1. 先从缓存查
        String key = CACHE_KEY + id;
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            System.out.println("缓存命中: " + key);
            return user;
        }

        // 2. 缓存未命中,查数据库
        user = this.getById(id);
        if (user != null) {
            // 3. 写入缓存,设置过期时间 30 分钟
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        }
        return user;
    }

    @Override
    public boolean updateUser(User user) {
        boolean success = this.updateById(user);
        if (success) {
            // 更新后删除缓存,下次查询重新加载
            redisTemplate.delete(CACHE_KEY + user.getId());
        }
        return success;
    }
}

缓存策略: 更新时直接删缓存,而不是更新缓存。下次查询时再重新加载,这叫 Cache-Aside 模式。

2. Spring Cache 注解版(更省事)

@CacheConfig(cacheNames = "user")
@Service
public class UserCacheService {

    @Cacheable(key = "#id", unless = "#result == null")
    public User getById(Long id) {
        // 方法内有缓存时不会执行
        // 等价于上面手动查缓存→查数据库→写缓存
        return userMapper.selectById(id);
    }

    @CachePut(key = "#user.id")
    public User update(User user) {
        userMapper.updateById(user);
        return user;
        // @CachePut 每次都会执行方法,并把返回值更新到缓存
    }

    @CacheEvict(key = "#id")
    public void delete(Long id) {
        userMapper.deleteById(id);
        // @CacheEvict 执行后删除缓存
    }
}

开启注解支持:

@SpringBootApplication
@EnableCaching  // 开启缓存注解
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

三种注解总结:

注解作用使用场景
@Cacheable先查缓存,有则返回,无则执行方法并写入缓存查询接口
@CachePut执行方法,并将结果更新到缓存更新接口
@CacheEvict执行方法,删除缓存删除接口

注意: @Cacheable 的 key 不要用 SpEL 拼接太复杂的表达式,出错了缓存会静默失效。

3. 缓存穿透/击穿/雪崩

// 缓存穿透:查一个不存在的 key,每次都穿透到数据库
// 解决方案:缓存空值(设置短过期时间)
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
    return user;
}
user = this.getById(id);
if (user == null) {
    // 缓存空值,防止穿透
    redisTemplate.opsForValue().set(key, new User(), 60, TimeUnit.SECONDS);
} else {
    redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
return user;
// 缓存雪崩:大量 key 同时过期
// 解决方案:过期时间加随机值
long baseExpire = 30;  // 基础 30 分钟
long randomExpire = ThreadLocalRandom.current().nextLong(5, 15);  // 随机加 5~15 分钟
redisTemplate.opsForValue().set(key, user, baseExpire + randomExpire, TimeUnit.MINUTES);

三、场景二:分布式锁

单体项目用 synchronized,分布式系统必须用 Redis 分布式锁。

1. 最简单的分布式锁

private static final String LOCK_KEY = "lock:order:";

public boolean createOrder(Long productId, Long userId) {
    String lockKey = LOCK_KEY + userId;

    // SETNX:key 不存在才设置成功,防止重复提交
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

    if (Boolean.FALSE.equals(locked)) {
        return false;  // 别人正在操作,请稍后
    }

    try {
        // 业务逻辑:扣库存、生成订单
        return doCreateOrder(productId, userId);
    } finally {
        // 释放锁(必须放在 finally 中)
        // 注意:只释放自己的锁,别把别人的锁误删了
        String value = (String) redisTemplate.opsForValue().get(lockKey);
        if ("1".equals(value)) {
            redisTemplate.delete(lockKey);
        }
    }
}

2. 更完善的锁工具类

@Component
public class RedisLock {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 加锁
     * @param key 锁的 key
     * @param value 锁的值(用于释放时校验)
     * @param expire 过期时间(秒)
     */
    public boolean lock(String key, String value, long expire) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue().setIfAbsent(key, value, expire, TimeUnit.SECONDS)
        );
    }

    /**
     * 解锁(使用 Lua 脚本保证原子性)
     */
    public boolean unlock(String key, String value) {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "  return redis.call('del', KEYS[1]) " +
            "else " +
            "  return 0 " +
            "end";
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            value
        );
        return Long.valueOf(1).equals(result);
    }
}

// 使用
RedisLock lock = new RedisLock();
String lockValue = UUID.randomUUID().toString();
try {
    boolean ok = lock.lock("lock:pay:" + orderNo, lockValue, 30);
    if (!ok) {
        return "操作太频繁,请稍后重试";
    }
    // 执行业务...
} finally {
    lock.unlock("lock:pay:" + orderNo, lockValue);
}

一定要用 Lua 脚本释放锁,检查自己的锁 + 删除是两步操作,非原子操作可能误删别人的锁。

四、场景三:Session 共享

多实例部署时,用户登录到 A 机器,下次请求被转发到 B 机器,Session 就丢了。用 Redis 存 Session 就能解决。

1. 引入依赖

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

2. 配置

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)  // Session 30 分钟过期
public class SessionConfig {
}

就这两步,Spring Session 自动把 Session 存储从内存换成 Redis。不需要改任何业务代码。

# application.yml 加一个配置项(可选)
server:
  servlet:
    session:
      timeout: 1800  # Session 超时时间

验证:

@RestController
public class SessionController {

    @PostMapping("/login")
    public String login(HttpSession session, @RequestParam String username) {
        session.setAttribute("user", username);  // 存到 Redis
        return "登录成功";
    }

    @GetMapping("/currentUser")
    public String currentUser(HttpSession session) {
        return (String) session.getAttribute("user");  // 从 Redis 取
    }
}

不管部署多少个实例,Session 数据都从 Redis 读取,用户登录状态在多台机器间共享。

五、常用数据类型操作速查

// String(字符串)
redisTemplate.opsForValue().set(key, value);
redisTemplate.opsForValue().get(key);
redisTemplate.opsForValue().increment(key);    // 自增(原子操作)
redisTemplate.opsForValue().decrement(key);    // 自减

// Hash(哈希)
redisTemplate.opsForHash().put(key, field, value);
redisTemplate.opsForHash().get(key, field);
redisTemplate.opsForHash().entries(key);       // 获取所有 field-value

// List(列表)
redisTemplate.opsForList().leftPush(key, value);   // 左入队
redisTemplate.opsForList().rightPop(key);          // 右出队
redisTemplate.opsForList().range(key, 0, -1);      // 获取全部

// Set(集合,无重复)
redisTemplate.opsForSet().add(key, values);
redisTemplate.opsForSet().members(key);
redisTemplate.opsForSet().isMember(key, value);    // 判断是否存在

// ZSet(有序集合)
redisTemplate.opsForZSet().add(key, value, score);
redisTemplate.opsForZSet().range(key, 0, -1);      // 按分数升序

// 通用操作
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);  // 设置过期时间
redisTemplate.delete(key);                              // 删除 key
redisTemplate.hasKey(key);                              // 判断是否存在

六、实际开发注意事项

  1. RedisTemplate 的 key 统一加前缀:比如 user:123order:2024001,方便管理
  2. 所有 key 必须设置过期时间:除非是计数器等极少数场景,否则 Redis 内存会爆
  3. 大 key 问题:不要往 Redis 里存超过 10MB 的数据(比如用户头像 base64)
  4. Redis 不是万能的:Redis 是缓存数据库,不是主数据库,重要数据必须落 MySQL

💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值