JAVA - 二层缓存设计(本地缓冲+redis缓冲+广播所有本地缓冲失效) demo

文章目录

  • 前言
    • [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>
相关推荐
YDS8293 小时前
DeepSeek RAG&MCP + Agent智能体项目 —— RAG知识库的搭建和接口实现
java·ai·springboot·agent·rag·deepseek
隔窗听雨眠4 小时前
多活部署、CDN加速与边缘缓存全链路优化实战
缓存
未若君雅裁5 小时前
MyBatis 一级缓存、二级缓存与清理机制
java·缓存·mybatis
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第65题】【JVM篇】第25题:谈谈对 OOM 的认识
java·开发语言·jvm
阿维的博客日记6 小时前
Nacos 为什么能让配置动态生效?(涉及 @RefreshScope 注解)
java·spring
雨辰AI6 小时前
SpringBoot3 + 人大金仓读写分离 + 分库分表 + 集群高可用 全栈实战
java·数据库·mysql·政务
Mr. zhihao7 小时前
深入解析redis基本数据结构
数据结构·数据库·redis
辰海Coding7 小时前
MiniSpring框架学习-完成的 IoC 容器
java·spring boot·学习·架构
小小编程路7 小时前
C++ 多线程与并发
java·jvm·c++