Spring Boot 使用 Redis 缓存:从基础到高级实践

引言

在现代分布式系统中,缓存是提高应用性能的关键手段之一。Redis 是一种流行的内存数据库,适用于构建高性能缓存系统。Spring Boot 提供了对 Redis 的良好集成,通过简单的注解配置即可轻松实现缓存功能。本文将详细介绍如何在 Spring Boot 中使用 Redis 缓存,并讨论常见缓存问题及其解决方案。


一、Spring Boot 集成 Redis 缓存

1.1 添加依赖

首先,在 pom.xml 文件中添加 Spring Data Redis 和 Lettuce(Redis 客户端)的依赖:

<dependencies>
    <!-- Spring Boot Starter Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Lettuce Redis Client -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
    </dependency>
</dependencies>

1.2 配置 Redis 连接

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

spring:
  redis:
    host: localhost
    port: 6379
    password: your_password # 如果有密码的话
    timeout: 6000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms

1.3 启用缓存

在主类上启用缓存支持:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

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

1.4 配置 Redis 缓存管理器

创建一个配置类来配置 Redis 缓存管理器:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;

@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // 设置默认缓存过期时间为10分钟
                .disableCachingNullValues() // 禁止缓存空值
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 使用 JSON 序列化

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

说明:

  • entryTtl(Duration.ofMinutes(10)):设置缓存项的默认过期时间为10分钟。
  • disableCachingNullValues():禁止缓存空值,防止无效数据进入缓存。
  • serializeValuesWith():指定使用 GenericJackson2JsonRedisSerializer 进行序列化,确保缓存中的数据以 JSON 格式存储。

1.5 使用缓存注解

现在你可以在服务层方法上使用 @Cacheable@CachePut@CacheEvict 注解来管理缓存。

示例代码

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

@Service
public class UserService {

    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        System.out.println("Fetching from database");
        return new User(id, "John Doe"); // 模拟从数据库获取用户
    }

    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        System.out.println("Updating in database");
        return user; // 模拟更新用户
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        System.out.println("Deleting from database");
    }
}

class User {
    private Long id;
    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // Getters and Setters
}

注解解释:

  • @Cacheable:用于查询操作,当缓存中存在该键时直接返回缓存结果;否则执行方法并将结果放入缓存。
  • @CachePut:用于更新操作,无论缓存中是否存在该键,都会执行方法并将结果放入缓存。
  • @CacheEvict:用于删除操作,从缓存中移除指定键的数据。

1.6 测试缓存效果

你可以通过编写单元测试或直接运行应用程序来验证缓存的效果。第一次调用 getUserById 方法时会打印“Fetching from database”,后续相同 ID 的调用则不会再次打印该消息,因为结果已经缓存。

单元测试示例

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testGetUserById() {
        User user1 = userService.getUserById(1L);
        User user2 = userService.getUserById(1L);

        System.out.println(user1 == user2); // 输出 true,表示两次调用返回的是同一个对象
    }
}

二、缓存一致性问题

缓存一致性是指确保缓存中的数据与数据库中的数据保持一致。在实际应用中,由于缓存和数据库之间存在时间差,可能会出现不一致的情况。

2.1 数据库与缓存双写

最常见的解决方案是采用数据库与缓存双写策略。即在更新数据库的同时更新缓存,或者删除缓存项以便下次查询时重新生成。

示例代码

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

@Service
public class UserService {

    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        System.out.println("Fetching from database");
        return new User(id, "John Doe"); // 模拟从数据库获取用户
    }

    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        System.out.println("Updating in database");
        return user; // 模拟更新用户
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        System.out.println("Deleting from database");
    }
}

解释:

  • @CachePut:用于更新操作,确保缓存中的数据与数据库同步。
  • @CacheEvict:用于删除操作,确保缓存中的数据与数据库同步。

2.2 延迟双删策略

为了避免并发情况下数据不一致的问题,可以采用延迟双删策略。具体步骤如下:

  1. 更新数据库。
  2. 删除缓存。
  3. 延迟一段时间后再次删除缓存。

这种方法可以有效避免高并发场景下的缓存不一致问题。

示例代码

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Async
    @CacheEvict(value = "users", key = "#id")
    public void deleteUserAsync(Long id) throws InterruptedException {
        Thread.sleep(1000); // 模拟延迟
        System.out.println("Deleting from cache again after delay");
    }

    public void deleteUser(Long id) {
        // 更新数据库逻辑
        System.out.println("Deleting from database");

        // 删除缓存
        deleteUserAsync(id);
    }
}

解释:

  • @Async:异步执行删除缓存的操作,确保延迟一段时间后再删除缓存。
  • Thread.sleep(1000):模拟延迟,实际应用中可以根据需要调整延迟时间。

三、缓存击穿、缓存穿透和缓存雪崩

3.1 缓存击穿

缓存击穿是指某个热点数据在缓存失效的瞬间,大量请求同时访问该数据,导致这些请求都直接打到数据库上,造成数据库压力过大。

解决方案

  • 设置热点数据永不过期:对于一些热点数据,可以将其缓存设置为永不过期,从而避免缓存击穿。

    @Cacheable(value = "hotUsers", key = "#id", unless = "#result == null", cacheManager = "customCacheManager")
    public User getHotUserById(Long id) {
        System.out.println("Fetching hot user from database");
        return new User(id, "Hot User"); // 模拟从数据库获取热点用户
    }
    
  • 加锁机制:在获取缓存数据时加上互斥锁,确保同一时刻只有一个线程去数据库查询并更新缓存。

    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    @Service
    public class UserService {
    
        private final Lock lock = new ReentrantLock();
    
        @Cacheable(value = "users", key = "#id")
        public User getUserById(Long id) {
            lock.lock();
            try {
                System.out.println("Fetching from database");
                return new User(id, "John Doe"); // 模拟从数据库获取用户
            } finally {
                lock.unlock();
            }
        }
    }
    

3.2 缓存穿透

缓存穿透是指恶意请求查询不存在的数据,导致每次请求都会直接访问数据库,无法利用缓存缓解数据库压力。

解决方案

  • 缓存空对象:对于查询不到的数据,也可以将其缓存起来,并设置较短的过期时间,避免频繁查询。

    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        System.out.println("Fetching from database");
        return null; // 模拟查询不到数据
    }
    
  • 布隆过滤器:在查询之前先通过布隆过滤器判断是否存在该数据,如果不存在则直接返回,避免不必要的数据库查询。

    import com.google.common.hash.BloomFilter;
    import com.google.common.hash.Funnels;
    
    import java.nio.charset.Charset;
    
    @Service
    public class UserService {
    
        private BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 1000);
    
        public boolean existsInBloomFilter(String id) {
            return bloomFilter.mightContain(id);
        }
    
        @Cacheable(value = "users", key = "#id", unless = "#result == null")
        public User getUserById(Long id) {
            if (!existsInBloomFilter(id.toString())) {
                return null; // 数据不存在,直接返回
            }
    
            System.out.println("Fetching from database");
            return new User(id, "John Doe"); // 模拟从数据库获取用户
        }
    }
    

3.3 缓存雪崩

缓存雪崩是指在同一时间段内,大量缓存数据同时失效,导致所有请求都直接打到数据库上,造成数据库崩溃。

解决方案

  • 随机过期时间:为每个缓存项设置不同的过期时间,避免大量缓存同时失效。

    @Cacheable(value = "users", key = "#id", ttl = "#{T(java.lang.Math).random() * 60 + 60}")
    public User getUserById(Long id) {
        System.out.println("Fetching from database");
        return new User(id, "John Doe"); // 模拟从数据库获取用户
    }
    
  • 多级缓存:引入多级缓存机制,例如本地缓存和分布式缓存结合使用,减轻单一缓存层的压力。

    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    @Service
    public class UserService {
    
        private Map<Long, User> localCache = new ConcurrentHashMap<>();
    
        @Cacheable(value = "users", key = "#id")
        public User getUserById(Long id) {
            if (localCache.containsKey(id)) {
                return localCache.get(id); // 先从本地缓存获取
            }
    
            System.out.println("Fetching from database");
            User user = new User(id, "John Doe"); // 模拟从数据库获取用户
            localCache.put(id, user); // 将数据放入本地缓存
            return user;
        }
    }
    

四、总结

在 Spring Boot 中使用 Redis 缓存可以通过简单的注解配置实现高效的数据缓存管理。然而,缓存的一致性问题以及缓存击穿、穿透和雪崩等挑战需要我们特别关注。通过合理的策略和技术手段,我们可以有效地应对这些问题,确保系统的稳定性和高性能。

希望这篇文章能够帮助你更好地理解和应用 Redis 缓存技术。如果你有任何问题或需要进一步的帮助,请随时告诉我!

文章作者: LibSept24_
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LibSept24_
Redis
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝