java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > 分布式环境使用MyBatis二级缓存

在分布式环境下正确使用MyBatis二级缓存的最佳实践

作者:一叶飘零_sweeeet

缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存,我们可以避免频繁与数据库进行交互,从而提高响应速度,这篇文章主要介绍了在分布式环境下正确使用MyBatis二级缓存的最佳实践,需要的朋友可以参考下

前言

在分布式环境下使用 MyBatis 二级缓存,核心挑战是解决多节点缓存一致性问题。单机环境中,二级缓存是内存级别的本地缓存,而分布式环境下多节点独立部署,本地缓存无法跨节点共享,易导致 “缓存孤岛” 和数据不一致。本文从底层原理出发,提供一套完整的分布式二级缓存解决方案,包含实战配置与最佳实践。

一、分布式环境下二级缓存的核心问题

在分布式架构(如微服务集群)中,默认的 MyBatis 二级缓存(本地内存缓存)会暴露三个致命问题:

二、解决方案:基于集中式缓存的二级缓存改造

分布式环境下的核心解决方案是:用集中式缓存(如 Redis、Memcached)替代本地内存缓存,让所有节点共享同一缓存源,实现缓存数据全局一致。

2.1 技术选型:MyBatis + Redis(最常用组合)

Redis 作为高性能的分布式缓存中间件,支持数据持久化、过期策略和集群模式,是 MyBatis 二级缓存的理想选择。实现思路是:

三、实战:MyBatis 集成 Redis 实现分布式二级缓存

3.1 环境准备

3.2 依赖配置(Maven)

<!-- MyBatis核心依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

<!-- Redis缓存依赖(MyBatis官方适配) -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

<!-- Redis客户端 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.3 配置 Redis 连接

src/main/resources下创建redis.properties,配置 Redis 连接信息:

# Redis服务器地址
redis.host=192.168.1.100
# Redis端口
redis.port=6379
# 连接超时时间(毫秒)
redis.timeout=2000
# Redis密码(无密码则留空)
redis.password=your_redis_password
# 数据库索引(默认0)
redis.database=1
# 缓存默认过期时间(毫秒,30分钟)
redis.default.expiration=1800000

3.4 改造实体类:实现序列化

分布式缓存中,对象需在网络中传输,必须实现Serializable接口,否则会导致缓存失败。

User.java

package com.example.entity;

import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 用户实体类(必须实现Serializable)
 */
@Data
public class User implements Serializable {
    // 序列化版本号(避免反序列化冲突)
    private static final long serialVersionUID = 1L;
    
    private Long id;
    private String username;
    private String email;
    private LocalDateTime createTime;
}

3.5 配置 Mapper 使用 Redis 缓存

在 Mapper.xml 中指定缓存类型为 Redis,替代默认的本地缓存。

UserMapper.xml

<?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="com.example.mapper.UserMapper">
    <!-- 配置Redis作为二级缓存 -->
    <cache 
        type="org.mybatis.caches.redis.RedisCache"  <!-- 指定Redis缓存实现类 -->
        eviction="LRU"  <!-- 缓存淘汰策略:最近最少使用 -->
        flushInterval="300000"  <!-- 自动刷新间隔(5分钟) -->
        size="1000"  <!-- 最大缓存对象数量 -->
        readOnly="false"/>  <!-- 非只读(需序列化) -->

    <!-- 查询语句:默认使用二级缓存 -->
    <select id="selectById" resultType="com.example.entity.User">
        SELECT id, username, email, create_time AS createTime
        FROM t_user
        WHERE id = #{id}
    </select>

    <!-- 更新语句:默认触发缓存清空(flushCache=true) -->
    <update id="update">
        UPDATE t_user
        SET username = #{username}, email = #{email}
        WHERE id = #{id}
    </update>
</mapper>

关键配置说明

3.6 全局启用二级缓存

在 MyBatis 配置文件(或 Spring Boot 配置)中确保二级缓存全局开启(默认开启,建议显式配置)。

application.yml

mybatis:
  configuration:
    cache-enabled: true  # 全局启用二级缓存(默认true)
  mapper-locations: classpath:mapper/*.xml  # 指定Mapper.xml路径

3.7 验证分布式缓存效果

部署两个服务节点(Node1 和 Node2),通过测试验证缓存一致性:

测试代码(Service 层)

@Slf4j
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    /**
     * 查询用户:优先从Redis缓存获取
     */
    public User getUserById(Long id) {
        if (Objects.isNull(id)) {
            log.warn("用户ID为空");
            return null;
        }
        User user = userMapper.selectById(id);
        log.info("查询用户结果:{}", user);
        return user;
    }

    /**
     * 更新用户:触发Redis缓存清空
     */
    @Transactional
    public void updateUser(User user) {
        if (Objects.isNull(user) || Objects.isNull(user.getId())) {
            log.warn("用户信息不完整");
            return;
        }
        int rows = userMapper.update(user);
        log.info("更新用户影响行数:{}", rows);
        // 事务提交后,MyBatis会自动清空Redis中该Mapper的缓存
    }
}

测试步骤与预期结果

通过 Redis 客户端(如redis-cli)可观察到缓存键值的创建与删除,证明所有节点共享同一缓存。

四、分布式缓存的高级优化策略

4.1 缓存键设计:避免命名冲突

MyBatis 默认的缓存键由namespace + SQL语句 + 参数组成,在分布式环境下需确保唯一性。可通过自定义RedisCache实现类优化键名:

CustomRedisCache.java

package com.example.cache;

import org.mybatis.caches.redis.RedisCache;
import java.util.UUID;

/**
 * 自定义Redis缓存,添加应用前缀避免键冲突
 */
public class CustomRedisCache extends RedisCache {
    // 应用唯一标识(避免多应用共用Redis时键冲突)
    private static final String APP_PREFIX = "myapp:";

    public CustomRedisCache(String id) {
        super(id);
    }

    /**
     * 重写缓存键,添加应用前缀
     */
    @Override
    public Object getObject(Object key) {
        String cacheKey = APP_PREFIX + key.toString();
        return super.getObject(cacheKey);
    }

    @Override
    public void putObject(Object key, Object value) {
        String cacheKey = APP_PREFIX + key.toString();
        super.putObject(cacheKey, value);
    }

    @Override
    public Object removeObject(Object key) {
        String cacheKey = APP_PREFIX + key.toString();
        return super.removeObject(cacheKey);
    }
}

在 Mapper.xml 中使用自定义缓存:

<cache type="com.example.cache.CustomRedisCache"/>

4.2 缓存失效策略:主动 + 被动结合

分布式环境下,单一的自动失效可能存在延迟,需结合主动失效策略:

/**
 * 手动删除指定用户的缓存
 */
public void deleteUserCache(Long userId) {
    // 获取UserMapper的缓存对象
    Cache cache = sqlSessionFactory.getConfiguration().getCache("com.example.mapper.UserMapper");
    if (Objects.nonNull(cache)) {
        // 构造缓存键(需与MyBatis生成规则一致)
        // 键格式:namespace + "::" + SQLID + "::" + 参数
        String cacheKey = "com.example.mapper.UserMapper::selectById::" + userId;
        cache.removeObject(cacheKey);
        log.info("手动删除用户缓存,key: {}", cacheKey);
    }
}

4.3 处理缓存与数据库一致性:延迟双删

在高并发场景,更新数据库后立即删除缓存可能仍有风险(删除缓存前已有请求读取旧缓存)。可采用 “延迟双删” 策略:

@Transactional
public void updateUserWithDelayDelete(User user) {
    // 1. 更新数据库
    userMapper.update(user);
    // 2. 第一次删除缓存(事务提交后执行)
    transactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            // 事务提交后删除缓存
            deleteUserCache(user.getId());
            
            // 3. 延迟1秒后第二次删除(避免更新前的请求仍读取旧缓存)
            CompletableFuture.runAsync(() -> {
                try {
                    Thread.sleep(1000);
                    deleteUserCache(user.getId());
                } catch (InterruptedException e) {
                    log.error("延迟删除缓存失败", e);
                }
            });
        }
    });
}

4.4 缓存序列化优化:使用 JSON 替代 Java 序列化

默认情况下,MyBatis-Redis 使用 Java 序列化存储对象,存在性能差、可读性低的问题。可自定义序列化方式(如 JSON):

JsonRedisCache.java(简化版)

package com.example.cache;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class JsonRedisCache implements Cache {
    private final String id;
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public JsonRedisCache(String id) {
        this.id = id;
        // 注入RedisTemplate(实际需通过Spring上下文获取)
        this.redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
    }

    @Override
    public String getId() { return id; }

    @Override
    public void putObject(Object key, Object value) {
        try {
            String jsonValue = objectMapper.writeValueAsString(value);
            redisTemplate.opsForValue().set(key.toString(), jsonValue, 30, TimeUnit.MINUTES);
        } catch (Exception e) {
            log.error("缓存序列化失败", e);
        }
    }

    @Override
    public Object getObject(Object key) {
        try {
            String jsonValue = (String) redisTemplate.opsForValue().get(key.toString());
            if (StringUtils.hasText(jsonValue)) {
                // 根据实际类型反序列化(简化示例)
                return objectMapper.readValue(jsonValue, User.class);
            }
        } catch (Exception e) {
            log.error("缓存反序列化失败", e);
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        redisTemplate.delete(key.toString());
        return null;
    }

    @Override
    public void clear() {
        // 清空当前namespace的所有缓存(需批量删除匹配键)
    }

    @Override
    public int getSize() { return 0; }

    @Override
    public ReadWriteLock getReadWriteLock() { return readWriteLock; }
}

使用 JSON 序列化后,Redis 中缓存的数据可读性更强,且序列化效率更高。

五、分布式二级缓存的适用场景与禁忌

5.1 适用场景

5.2 禁忌场景

六、监控与调优

七、总结

分布式环境下正确使用 MyBatis 二级缓存的核心是用集中式缓存(如 Redis)替代本地缓存,关键步骤包括:

通过这套方案,既能保留二级缓存的性能优势,又能解决分布式环境的数据一致性问题,实现 “高性能 + 高可靠” 的平衡。

到此这篇关于在分布式环境下正确使用MyBatis二级缓存的文章就介绍到这了,更多相关分布式环境使用MyBatis二级缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文