java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > mybatis二级缓存修复

MyBatis 原生二级缓存"难以修复"的原因解析及解决方案

作者:旷野说

文章主要讨论了MyBatis原生二级缓存存在的问题,包括结构性缺陷、难以修复等,社区提供了多种增强插件方案,但这些方案也有各自的优缺点,本文结合实例代码介绍的非常详细,感兴趣的朋友跟随小编一起看看吧

🚫 一、为什么 MyBatis 原生二级缓存“难以修复”?

MyBatis 二级缓存的底层设计存在结构性缺陷,不是加个插件就能完美解决的:

问题原因高并发影响
缓存 Key 粒度粗糙Key = namespace + sql + params,但 params 是对象哈希,分页/排序等参数容易哈希冲突或忽略语义差异返回错误数据(如分页错乱)
无 TTL / 过期机制默认使用 PerpetualCache,数据永不过期内存泄漏 + 脏读
跨 JVM 无法共享缓存是本地内存(JVM 内),多实例部署时各节点缓存不一致数据分裂,用户看到不同结果
写操作不自动失效关联缓存更新 User 表,不会自动清理 OrderMapper 中关联的 user_orders 缓存脏读持续存在

⚠️ 这些是架构级缺陷,靠拦截器或装饰器插件无法根本解决

🔧 二、社区增强插件方案(可选,但需谨慎)

虽然不能“修复”,但有插件替换缓存实现,提升可用性:

1.mybatis-redis(最常用)

@Mapper
@CacheNamespace(implementation = RedisCache.class)
public interface OrderMapper {
    // ...
}

💡 适用场景:读多写少、数据变更不频繁、对延迟不敏感的配置类数据(如字典表)

2.自定义 Cache 实现(继承 MyBatisCache接口)

你可以自己实现带版本号逻辑过期的缓存:

public class VersionedCache implements Cache {
    private final String id;
    private final RedisTemplate redis;
    @Override
    public void putObject(Object key, Object value) {
        // key = "order:123", value = { data: {...}, version: 20251203 }
        redis.opsForHash().put("mybatis:cache:" + id, serialize(key), wrapWithVersion(value));
    }
    @Override
    public Object getObject(Object key) {
        Object cached = redis.opsForHash().get("mybatis:cache:" + id, serialize(key));
        if (cached != null && isVersionValid(cached)) {
            return unwrap(cached);
        }
        return null;
    }
}

🛑 三、我们的选择:弃用二级缓存,自建业务缓存层

在高并发订单系统中,我们最终彻底关闭 MyBatis 二级缓存,原因如下:

  1. 控制权不足:MyBatis 缓存是“黑盒”,无法插入降级、熔断、监控逻辑
  2. 异常难追溯:缓存脏数据问题往往延迟暴露,修复成本高
  3. 违背“可观测性”原则:缓存命中/失效无法对接 Prometheus / SkyWalking

✅ 替代方案:业务层 + Redis + 幂等 + 版本号

@Service
public class OrderService {
    public Order getOrder(Long id) {
        String cacheKey = "order:v2:" + id;
        Order order = redis.get(cacheKey, Order.class);
        if (order != null) return order;
        // 双检锁 + 空值缓存防穿透
        synchronized (getLockKey(id)) {
            order = redis.get(cacheKey, Order.class);
            if (order == null) {
                order = orderMapper.findById(id);
                if (order != null) {
                    redis.setex(cacheKey, 300, order); // 5分钟过期
                } else {
                    redis.setex("empty:" + cacheKey, 60, "1"); // 防缓存穿透
                }
            }
        }
        return order;
    }
    @Transactional
    public void updateOrder(Order order) {
        orderMapper.update(order);
        // 主动失效缓存(旁路删除)
        redis.delete("order:v2:" + order.getId());
    }
}

优势

🧠 总结:二级缓存不是“能不能修”,而是“值不值得用”

方案适合场景高并发推荐度
MyBatis 原生二级缓存单机、低频、只读数据❌ 不推荐
mybatis-redis 插件多实例、读多写少、容忍一定延迟⚠️ 谨慎评估
自定义 Cache 实现有强缓存治理能力的团队⚠️ 成本高
业务层自建缓存高并发、强一致性、可观测性要求高强烈推荐

最终口诀更新:

“二级缓存看似香,架构缺陷难躲藏;
分页排序易冲突,多机部署更遭殃;
插件替换治标难,自建缓存才稳当;
Key 带版本加 TTL,高并发下不慌张!”

如果你正在设计高并发系统,放弃对 MyBatis 二级缓存的幻想,把缓存控制权拿回业务层,才是真正的“韧性设计”。

到此这篇关于MyBatis 原生二级缓存“难以修复”的原因解析及解决方案的文章就介绍到这了,更多相关mybatis二级缓存修复内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

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