如果你对 SpringBoot 缓存机制尚不熟悉,建议先阅读之前介绍 SpringBoot 集成 Caffeine 实现本地缓存的相关文章。今天我们来探讨一个重要问题:为什么需要引入分布式缓存?
在实际项目开发中,单机应用往往难以承受业务压力,随着系统规模增长,通常需要采用分布式部署。然而,分布式架构会带来一个典型问题:每个服务实例都拥有独立的本地缓存,彼此之间无法共享数据。更棘手的是,当某个服务更新了缓存后,其他服务无法感知到这一变化,用户请求若落到其他节点,读取到的仍是旧数据,导致缓存不一致。此时,许多开发者会想到借助 Redis 来解决,但 Redis 作为独立的第三方中间件,存在网络延迟开销,且所有缓存请求都集中到 Redis 上,在高并发场景下 Redis 可能成为性能瓶颈,甚至直接宕机。因此,一个更可靠的设计思路是:将本地缓存(如 Caffeine)与 Redis 分布式缓存相结合,并利用 Redis 的发布订阅(Pub/Sub)机制,当某个服务更新缓存时,主动通知其他服务同步更新,从而保证缓存一致性。
接下来,我将介绍一个基于上述思路开源的轻量级分布式缓存工具——tools-redis,它正是围绕这一设计实现的。
一、引入 Maven 依赖
cn.gjing
tools-redis
1.2.0
二、在启动类上添加缓存注解
/**
* @author Gjing
*/
@SpringBootApplication
@EnableToolsCache
public class TestRedisApplication {
public static void main(String[] args) {
SpringApplication.run(TestRedisApplication.class, args);
}
}
三、配置参数说明
1. 配置项一览
所有配置项均以 tools.cache 为前缀,支持按需自定义。以下是完整的配置项列表:
| 配置项 | 描述 |
|---|---|
| cache-names | 缓存 key 的名称 |
| cache-value-nullable | 是否允许缓存空值,默认 true,防止缓存穿透 |
| dynamic | 是否动态根据 cacheName 创建 Cache 实现,默认 true |
| cache-prefix | 缓存 key 的前缀 |
| caffeine.initial-capacity | Caffeine 初始化大小 |
| caffeine.expire-after-access | 访问后过期时间,单位毫秒 |
| caffeine.expire-after-write | 写入后过期时间,单位毫秒 |
| caffeine.maximum-size | 最大缓存对象个数,超过此数量时之前放入的缓存会失效 |
| caffeine.refresh-after-write | 写入后刷新时间,单位毫秒 |
| redis.every-cache-expire | 每个 cacheName 的过期时间,单位秒,优先级比 expire 高 |
| redis.expire | 全局过期时间,单位秒,默认不过期 |
| redis.topic | 缓存更新时通知其他节点的 topic 名称 |
2. 配置示例
YAML 配置方式
tools:
cache:
cache-prefix: 锁的前缀
redis:
expire: 10
caffeine:
expire-after-write: 3000
JavaBean 配置方式
/**
* @author Gjing
*/
@Configuration
public class CacheConfiguration {
@Bean
public ToolsCache toolsCache() {
return ToolsCache.builder()
.cachePrefix("锁的前缀")
.dynamic(true)
.build();
}
@Bean
public RedisCache redisCache() {
return RedisCache.builder()
.expire(10)
.build();
}
@Bean
public CaffeineCache caffeineCache() {
return CaffeineCache.builder()
.expireAfterWrite(3000)
.build();
}
}
三、快速使用示例
/**
* @author Gjing
*/
@Service
@Slf4j
public class CustomService {
@Resource
private CustomRepository customRepository;
/**
* 获取一个用户
* @param customId 用户id
* @return Custom
*/
@Cacheable(value = "user", key = "#customId")
public Custom getCustom(Integer customId) {
log.warn("查询数据库用户信息");
return customRepository.findById(customId)
.orElseThrow(() -> new NullPointerException("User is not exist"));
}
/**
* 删除一个用户
* @param customId 用户id
*/
@CacheEvict(value = "user", key = "#customId")
public void deleteUser(Integer customId) {
Custom custom = customRepository.findById(customId)
.orElseThrow(() -> new NullPointerException("User is not exist"));
customRepository.delete(custom);
}
}
四、定义 REST 接口调用
/**
* @author Gjing
*/
@RestController
public class CustomController {
@Resource
private CustomService customService;
@GetMapping("/user/{custom-id}")
@ApiOperation(value = "查询用户", httpMethod = "GET")
public ResponseEntity getUser(@PathVariable("custom-id") Integer customId) {
return ResponseEntity.ok(customService.getCustom(customId));
}
@DeleteMapping("/user")
@ApiOperation(value = "删除用户", httpMethod = "DELETE")
@ApiImplicitParam(name = "customId", value = "用户Id", dataType = "int", required = true, paramType = "Query")
@NotNull
public ResponseEntity deleteUser(Integer customId) {
customService.deleteUser(customId);
return ResponseEntity.ok("Successfully delete");
}
}
实际调用结果如下所示:

如果在使用过程中遇到任何问题,欢迎留言交流,我会及时回复并持续更新项目。项目源码地址:tools-redis。
