文章目录
- 前言
-
- [JAVA - 二层缓存设计(本地缓冲+redis缓冲+广播所有本地缓冲失效) demo](#JAVA - 二层缓存设计(本地缓冲+redis缓冲+广播所有本地缓冲失效) demo)
-
- [1. 设计流程](#1. 设计流程)
-
- [1.1. 查询流程](#1.1. 查询流程)
- [1.2. 更新流程](#1.2. 更新流程)
- [1.3. 新增流程](#1.3. 新增流程)
- [1.4. 删除流程](#1.4. 删除流程)
- [2. 示例演示](#2. 示例演示)
-
- [2.1. 新增动作](#2.1. 新增动作)
- [2.2. 查询动作](#2.2. 查询动作)
- [2.3. 更新动作](#2.3. 更新动作)
- [2.4. 删除动作](#2.4. 删除动作)
- [3. demo源码](#3. demo源码)
-
- [3.1. 测试表结构](#3.1. 测试表结构)
- [3.2. pom 配置类](#3.2. pom 配置类)
- [3.3. 配置类](#3.3. 配置类)
- [3.4. Controller](#3.4. Controller)
- [3.5. entity](#3.5. entity)
- [3.6. mapper](#3.6. mapper)
- [3.7. Service](#3.7. Service)
- [3.8. vo](#3.8. vo)
- [3.9. xml](#3.9. xml)
前言
如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^ _ ^。
而且听说点赞的人每天的运气都不会太差,实在白嫖的话,那欢迎常来啊!!!
JAVA - 二层缓存设计(本地缓冲+redis缓冲+广播所有本地缓冲失效) demo
1. 设计流程
1.1. 查询流程
bash
1. 查 Caffeine
2. 未命中 → 查 Redis
3. Redis 命中 → 回写 Caffeine
4. Redis 未命中 → 查 DB
5. 写 Redis
6. 写 Caffeine
7. 返回
1.2. 更新流程
bash
1. 更新 DB
2. 删除 Redis Key
3. Redis Pub/Sub 发布失效消息
4. 所有节点删除本地 Caffeine
注意的mq使用redis是不是就可以了,同时一级缓存的时间要小于二级缓冲的时间。
1.3. 新增流程
bash
1. 插入 DB
2. 写 Redis
3. 写 L1
1.4. 删除流程
bash
1. 删除DB
2. 删除redis
3. 广播通知
4. 删除当前节点L1 // 缩短脏数据窗口
只有更新和删除的时候才需要广播,因为其他节点可能会有旧值,因此必须广播,对于本地缓冲的设计目的为按需缓冲,只有 节点真正访问某个 Key 时,才把它放进本地缓存。
注意的是这不是强一致性,而是最终一致性,因为删除 Redis和广播 MQ之间存在时间窗口,
在这个窗口期某节点可能L1 命中旧数据,不过行业一般接受这个风险,因为窗口通常 < 100ms,成本远低于强一致方案。
2. 示例演示
2.1. 新增动作


可以看到写入缓冲成功:

2.2. 查询动作


可以看到本地缓冲那块拦住了。
等待5分钟后,在此查询该接口,效果如下:

可以看到本地缓冲失效,触发redis缓冲。
当redis缓冲失效、击中db,如下:

2.3. 更新动作



可以看到,缓冲监听监听到该key后进行删除。
2.4. 删除动作
可以看到,缓冲监听监听到该key后进行删除。
3. demo源码
3.1. 测试表结构
bash
/*==============================================================*/
/* Table: t_user_test */
/*==============================================================*/
CREATE TABLE `t_user_test` (
`PK_ID` int(18) NOT NULL AUTO_INCREMENT COMMENT '顺序号',
`CODE` varchar(36) NOT NULL COMMENT 'CODE',
`NAME` varchar(200) NOT NULL COMMENT '姓名',
PRIMARY KEY (`PK_ID`),
UNIQUE KEY `U_tuser_test_01` (`CODE`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='测试表';
3.2. pom 配置类
bash
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
3.3. 配置类
CacheConstants:
java
package org.example.config.redis.constants;
public interface CacheConstants {
String USER_KEY_PREFIX = "user:";
String CACHE_INVALIDATE_TOPIC = "cache:invalidate";
long REDIS_TTL_SECONDS = 30 * 60; // 30分钟
}
CacheConfig:
java
package org.example.config.redis;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CacheConfig {
@Bean
public Cache<String, Object> localCache() {
return Caffeine.newBuilder()
.maximumSize(10_000) // 最多存 1 万条数据
.expireAfterWrite(5, TimeUnit.MINUTES) // 写入5分钟后过期
.build(); // 创建缓存实例
}
}
CacheMessage:
java
package org.example.config.redis;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CacheMessage implements Serializable {
private String cacheKey;
}
CacheMessageListener:
java
package org.example.config.redis;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 消息监听器
*/
@Slf4j
@Component
public class CacheMessageListener implements MessageListener {
@Resource(name = "localCache")
private Cache<String, Object> localCache;
@Resource
private Jackson2JsonRedisSerializer<Object> serializer;
@Override
public void onMessage(Message message, byte[] pattern) {
Object obj = serializer.deserialize(message.getBody());
if (!(obj instanceof CacheMessage)) {
return;
}
CacheMessage cacheMessage = (CacheMessage) obj;
localCache.invalidate(cacheMessage.getCacheKey());
log.info("收到缓存失效通知,删除本地缓存 key={}",
cacheMessage.getCacheKey());
}
}
RedisMessageListenerConfig:
java
package org.example.config.redis;
import org.example.config.redis.constants.CacheConstants;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* 订阅配置
*/
@Configuration
public class RedisMessageListenerConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
@Qualifier("redisStdTemplate")
RedisTemplate<String, Object> redisTemplate,
CacheMessageListener listener) {
RedisMessageListenerContainer container =
new RedisMessageListenerContainer();
if (redisTemplate.getConnectionFactory() != null) {
container.setConnectionFactory(redisTemplate.getConnectionFactory());
}
container.addMessageListener(
listener,
new ChannelTopic(CacheConstants.CACHE_INVALIDATE_TOPIC)
);
return container;
}
}
RedisStdConfig:
java
package org.example.config.redis;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @author yangzhenyu
* @version 1.0
* @description:
* @date 2023/8/2 16:14
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnExpression("${spring.redis.my.flag:false} == true")
@EnableConfigurationProperties({RedisStdProperties.class})
public class RedisStdConfig {
private static final Logger log = LoggerFactory.getLogger(RedisStdConfig.class);
public RedisStdConfig() {
log.info("===================集成 redis 配置===================");
}
private JedisConnectionFactory getJedisConnectionFactoryFromRedisLockProperties(RedisStdProperties redisStdProperties) {
JedisConnectionFactory factory = null;
JedisPoolConfig poolConfig = redisStdProperties.getPool() == null ? new JedisPoolConfig() : jedisPoolConfig(redisStdProperties);
if (redisStdProperties.getSentinel() != null) {
//Sentinel
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
RedisStdProperties.Sentinel sentinel = redisStdProperties.getSentinel();
redisSentinelConfiguration.setMaster(sentinel.getMaster());
List<String> nodesStr = sentinel.getNodes();
List<RedisNode> sentinels = new ArrayList<>();
for (String s : nodesStr) {
sentinels.add(new RedisNode(s.split(":")[0], Integer.parseInt(s.split(":")[1])));
}
redisSentinelConfiguration.setSentinels(sentinels);
if (redisStdProperties.getPassword() != null) {
redisSentinelConfiguration.setPassword(redisStdProperties.getPassword());
}
factory = new JedisConnectionFactory(redisSentinelConfiguration, poolConfig);
} else if (redisStdProperties.getCluster() != null) {
//Cluster
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
RedisStdProperties.Cluster cluster = redisStdProperties.getCluster();
Integer maxRedirects = cluster.getMaxRedirects();
clusterConfig.setMaxRedirects(maxRedirects);
List<String> nodes = cluster.getNodes();
List<RedisNode> clusterNodes = new ArrayList<>();
for (String clusterNode : nodes) {
clusterNodes.add(new RedisNode(clusterNode.split(":")[0], Integer.parseInt(clusterNode.split(":")[1])));
}
clusterConfig.setClusterNodes(clusterNodes);
// 设置集群的密码
if (redisStdProperties.getPassword() != null) {
clusterConfig.setPassword(redisStdProperties.getPassword()); // 为集群配置密码
}
factory = new JedisConnectionFactory(clusterConfig, poolConfig);
} else {
factory = new JedisConnectionFactory(poolConfig);
Objects.requireNonNull(factory.getStandaloneConfiguration()).setHostName(redisStdProperties.getHost());
factory.getStandaloneConfiguration().setPort(redisStdProperties.getPort());
}
Objects.requireNonNull(factory.getStandaloneConfiguration()).setDatabase(redisStdProperties.getDatabase());
if (redisStdProperties.getPassword() != null) {
factory.getStandaloneConfiguration().setPassword(redisStdProperties.getPassword());
}
factory.afterPropertiesSet();
return factory;
}
private JedisPoolConfig jedisPoolConfig(RedisStdProperties redisStdProperties) {
JedisPoolConfig config = new JedisPoolConfig();
RedisProperties.Pool props = redisStdProperties.getPool();
config.setMaxTotal(props.getMaxActive());
config.setMaxIdle(props.getMaxIdle());
config.setMinIdle(props.getMinIdle());
config.setMaxWaitMillis(props.getMaxWait().toMillis());
config.setTestWhileIdle(true);
return config;
}
//缓存操作组件
@Bean(name = "stringRedisStdTemplate")
public StringRedisTemplate stringRedisTemplate(@Autowired RedisStdProperties redisStdProperties){
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(getJedisConnectionFactoryFromRedisLockProperties(redisStdProperties));
return stringRedisTemplate;
}
/**
* RedisTemplate配置
*/
@Bean(name = "redisStdTemplate")
public RedisTemplate<String, Object> redisStdTemplate(@Autowired RedisStdProperties redisStdProperties, Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer) {
// 设置序列化
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(getJedisConnectionFactoryFromRedisLockProperties(redisStdProperties));
RedisSerializer<?> stringSerializer = new StringRedisSerializer();
// key序列化
redisTemplate.setKeySerializer(stringSerializer);
// value序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// Hash key序列化
redisTemplate.setHashKeySerializer(stringSerializer);
// Hash value序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 使用Jackson序列化对象
*/
@Bean
public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
serializer.setObjectMapper(objectMapper);
return serializer;
}
}
RedisStdProperties:
java
package org.example.config.redis;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* @author yangzhenyu
* @version 1.0
* @description:
* @date 2023/8/2 14:20
*/
@ConfigurationProperties(prefix = "spring.redis.my")
public class RedisStdProperties {
@Value("0")
private Integer database = 0;
private String url;
@Value("localhost")
private String host ;
@Value("")
private String password;
@Value("6379")
private Integer port ;
private boolean ssl;
private int timeout;
private RedisProperties.Pool pool;
private Sentinel sentinel;
private Cluster cluster;
//=============================================================== set get ====================================================
public int getDatabase() {
return this.database;
}
public void setDatabase(int database) {
this.database = database;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public String getHost() {
return this.host;
}
public void setHost(String host) {
this.host = host;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
public boolean isSsl() {
return this.ssl;
}
public void setSsl(boolean ssl) {
this.ssl = ssl;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getTimeout() {
return this.timeout;
}
public Sentinel getSentinel() {
return this.sentinel;
}
public void setSentinel(Sentinel sentinel) {
this.sentinel = sentinel;
}
public RedisProperties.Pool getPool() {
return this.pool;
}
public void setPool(RedisProperties.Pool pool) {
this.pool = pool;
}
public Cluster getCluster() {
return this.cluster;
}
public void setCluster(Cluster cluster) {
this.cluster = cluster;
}
public static class Sentinel {
private String master;
private List<String> nodes;
public Sentinel() {
//Do nothing
}
public String getMaster() {
return this.master;
}
public void setMaster(String master) {
this.master = master;
}
public List<String> getNodes() {
return this.nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}
public static class Cluster {
private List<String> nodes;
private Integer maxRedirects;
public Cluster() {
//Do nothing
}
public List<String> getNodes() {
return this.nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
public Integer getMaxRedirects() {
return this.maxRedirects;
}
public void setMaxRedirects(Integer maxRedirects) {
this.maxRedirects = maxRedirects;
}
}
public static class Pool {
private int maxIdle = 8;
private int minIdle = 0;
private int maxActive = 8;
private int maxWait = -1;
public Pool() {
//Do nothing
}
public int getMaxIdle() {
return this.maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public int getMinIdle() {
return this.minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public int getMaxActive() {
return this.maxActive;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public int getMaxWait() {
return this.maxWait;
}
public void setMaxWait(int maxWait) {
this.maxWait = maxWait;
}
}
}
3.4. Controller
TUserTestController:
java
package org.example.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.example.annotation.CommonLog;
import org.example.entity.TUserTest;
import org.example.exception.ExceptionEnum;
import org.example.exception.model.ResponseResult;
import org.example.exception.throwtype.RunException;
import org.example.service.TUserTestService;
import org.example.vo.TUserTestVo;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* <p>
* 双层缓存测试 前端控制器
* </p>
*
* @author yangzhenyu
* @since 2026-05-14 11:52:16
*/
@Api(value = "双层缓存测试", tags = {" 双层缓存测试"})
@Slf4j
@Validated
@RestController
@RequestMapping(value="api/tUserTest")
public class TUserTestController {
@Resource
private TUserTestService tUserTestService;
/**
* 根据 code 查询
*
* GET /tUserTest/{code}
*/
@GetMapping("/{code}")
public ResponseResult getById(@PathVariable("code") String code) {
if (StringUtils.isEmpty(code)){
throw new RunException(ExceptionEnum.ERROR_MSG, "code不能为空");
}
return ResponseResult.ok(tUserTestService.getById(code));
}
/**
* 新增
*
* POST /tUserTest
*/
@ApiOperation(value = "新增", notes = "新增")
@CommonLog(methodName = "新增",className = "ToolController#scan" ,url = "api/tUserTest/save")
@RequestMapping(value = "/save", method = RequestMethod.POST)
public ResponseResult save(@Validated @RequestBody TUserTestVo user) {
int rows = tUserTestService.save(user);
if (rows > 0) {
return ResponseResult.ok("新增成功");
}
throw new RunException(ExceptionEnum.ERROR_MSG, "新增失败");
}
/**
* 更新
*
* PUT /tUserTest
*/
@ApiOperation(value = "update", notes = "update")
@CommonLog(methodName = "update",className = "update" ,url = "api/tUserTest/update")
@RequestMapping(value = "/update", method = RequestMethod.POST)
public ResponseResult update(@Validated @RequestBody TUserTestVo user) {
int rows = tUserTestService.update(user);
if (rows > 0) {
return ResponseResult.ok("更新成功");
}
throw new RunException(ExceptionEnum.ERROR_MSG, "更新失败");
}
/**
* 删除
*
* DELETE /tUserTest/{code}
*/
@CommonLog(methodName = "删除",className = "删除" ,url = "-")
@DeleteMapping("/{code}")
public ResponseResult delete(@PathVariable("code") String code) {
if (StringUtils.isEmpty(code)){
throw new RunException(ExceptionEnum.ERROR_MSG, "code不能为空");
}
int rows = tUserTestService.delete(code);
if (rows > 0) {
return ResponseResult.ok("删除成功");
}
throw new RunException(ExceptionEnum.ERROR_MSG, "删除失败");
}
}
3.5. entity
java
package org.example.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 测试表
* </p>
*
* @author yangzhenyu
* @since 2026-05-14 11:52:16
*/
@Getter
@Setter
@TableName("t_user_test")
@ApiModel(value = "TUserTest对象", description = "测试表")
public class TUserTest implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("顺序号")
@TableId(value = "PK_ID", type = IdType.AUTO)
private Long pkId;
@ApiModelProperty("CODE")
@TableField("CODE")
private String code;
@ApiModelProperty("姓名")
@TableField("NAME")
private String name;
}
3.6. mapper
java
package org.example.mapper;
import org.example.entity.TUserTest;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* 测试表 Mapper 接口
* </p>
*
* @author yangzhenyu
* @since 2026-05-14 11:52:16
*/
@Mapper
public interface TUserTestMapper extends BaseMapper<TUserTest> {
}
3.7. Service
TUserTestService:
java
package org.example.service;
import org.example.entity.TUserTest;
import com.baomidou.mybatisplus.extension.service.IService;
import org.example.vo.TUserTestVo;
/**
* <p>
* 测试表 服务类
* </p>
*
* @author yangzhenyu
* @since 2026-05-14 11:52:16
*/
public interface TUserTestService extends IService<TUserTest> {
TUserTest getById(String code);
int update(TUserTestVo user);
int save(TUserTestVo user);
int delete(String code);
}
TUserTestServiceImpl:
java
package org.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.example.config.redis.CacheMessage;
import org.example.config.redis.constants.CacheConstants;
import org.example.entity.TUserTest;
import org.example.mapper.TUserTestMapper;
import org.example.service.TUserTestService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.vo.TUserTestVo;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
/**
* <p>
* 测试表 服务实现类
* </p>
*
* @author yangzhenyu
* @since 2026-05-14 11:52:16
*/
@Slf4j
@Service
public class TUserTestServiceImpl extends ServiceImpl<TUserTestMapper, TUserTest> implements TUserTestService {
@Resource(name = "localCache")
private Cache<String, Object> localCache;
@Resource(name = "redisStdTemplate")
private RedisTemplate<String, Object> redisTemplate;
@Resource
private TUserTestMapper mapper;
public TUserTest getById(String code) {
String key = CacheConstants.USER_KEY_PREFIX + code;
TUserTest localUser = (TUserTest) localCache.getIfPresent(key);
if (localUser != null) {
log.info("L1 Caffeine hit: {}", key);
return localUser;
}
// 2. 查询 Redis
TUserTest redisUser = (TUserTest) redisTemplate.opsForValue().get(key);
if (redisUser != null) {
log.info("L2 Redis hit: {}", key);
localCache.put(key, redisUser);
return redisUser;
}
// 3. 查询 DB
log.info("DB hit: {}", key);
QueryWrapper<TUserTest> wrapper = new QueryWrapper<>();
wrapper.eq("CODE", code);
TUserTest tUserTest = mapper.selectOne(wrapper);
if (null != tUserTest) {
// 写 Redis
redisTemplate.opsForValue().set(
key,
tUserTest,
CacheConstants.REDIS_TTL_SECONDS,
TimeUnit.SECONDS
);
// 写本地缓存
localCache.put(key, tUserTest);
}
return tUserTest;
}
@Transactional
public int update(TUserTestVo user) {
String key = CacheConstants.USER_KEY_PREFIX + user.getCode();
UpdateWrapper<TUserTest> updateWrapper = new UpdateWrapper<>();
TUserTest updated = new TUserTest();
BeanUtils.copyProperties(user,updated);
updateWrapper.eq("CODE", user.getCode());
updated.setCode(null);
// 1. 更新 DB
int update = mapper.update(updated, updateWrapper);
// 2. 删除 Redis
redisTemplate.delete(key);
// 3. 发布消息
redisTemplate.convertAndSend(
CacheConstants.CACHE_INVALIDATE_TOPIC,
new CacheMessage(key)
);
// 4. 当前节点也删除本地缓存(降低消息延迟窗口)
localCache.invalidate(key);
log.info("更新完成,缓存已失效 key={}", key);
return update;
}
@Transactional
public int save(TUserTestVo user) {
String key = CacheConstants.USER_KEY_PREFIX + user.getCode();
// 1. 插入数据库
TUserTest target = new TUserTest();
BeanUtils.copyProperties(user,target);
int insert = mapper.insert(target);
// 2. 写 Redis
redisTemplate.opsForValue().set(
key,
target,
CacheConstants.REDIS_TTL_SECONDS,
TimeUnit.SECONDS
);
// 3. 写本地缓存
localCache.put(key, target);
log.info("新增成功,写入缓存 key={}", key);
return insert;
}
@Transactional
public int delete(String code) {
String key = CacheConstants.USER_KEY_PREFIX + code;
// 1. 删除数据库
QueryWrapper<TUserTest> wrapper = new QueryWrapper<>();
wrapper.eq("CODE", code);
int delete = mapper.delete(wrapper);
// 2. 删除 Redis
redisTemplate.delete(key);
// 3. 广播通知
redisTemplate.convertAndSend(
CacheConstants.CACHE_INVALIDATE_TOPIC,
new CacheMessage(key)
);
// 4. 删除当前节点 L1
localCache.invalidate(key);
log.info("删除成功,缓存已清理 key={}", key);
return delete;
}
}
3.8. vo
TUserTestVo
java
package org.example.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import org.hibernate.validator.constraints.NotEmpty;
/**
* <p>
* 测试表
* </p>
*
* @author yangzhenyu
* @since 2026-05-14 11:52:16
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class TUserTestVo {
@ApiModelProperty("code")
@NotEmpty(message ="code不能为空")
private String code;
@ApiModelProperty("姓名")
@NotEmpty(message ="姓名不能为空")
private String name;
}
3.9. xml
java
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mapper.TUserTestMapper">
</mapper>