Java学习指南
  • Java 编程的逻辑
  • Java进阶
  • Java FrameWorks
  • 了解 USB Type-A,B,C 三大标准接口
  • 深入浅出DDD
  • 重构:改善既有代码的设计
  • 面试大纲
  • 云原生
    • 什么是无服务器(what is serverless)?
  • 博客
    • 深入分析Log4j 漏洞
  • 博客
    • Serverless之快速搭建Spring Boot应用
  • 博客
    • 使用 Prometheus + Grafana + Spring Boot Actuator 监控应用
  • 博客
    • 使用 Prometheus + Grafana 监控 MySQL
  • 博客
    • 使用Github Actions + Docker 部署Spring Boot应用
  • 博客
    • Redis分布式锁之Redisson的原理和实践
  • 博客
    • 数据库中的树结构应该怎样去设计
  • 学习&成长
    • 如何成为技术大牛
  • 开发工具
    • Git Commit Message Guidelines
  • 开发工具
    • git命名大全
  • 开发工具
    • Gradle vs Maven Comparison
  • 开发工具
    • Swagger2常用注解及其说明
  • 开发工具
    • 简明 VIM 练级攻略
  • 微服务
    • 十大微服务设计模式和原则
  • 微服务
    • 微服务下的身份认证和令牌管理
  • 微服务
    • 微服务坏味道之循环依赖
  • 设计模式
    • 设计模式 - JDK中的设计模式
  • 设计模式
    • 设计模式 - Java三种代理模式
  • 设计模式
    • 设计模式 - 六大设计原则
  • 设计模式
    • 设计模式 - 单例模式
  • 设计模式
    • 设计模式 - 命名模式
  • 设计模式
    • 设计模式 - 备忘录模式
  • 设计模式
    • 设计模式 - 概览
  • 设计模式
    • 设计模式 - 没用的设计模式
  • 质量&效率
    • Homebrew 替换国内镜像源
  • 质量&效率
    • 工作中如何做好技术积累
  • Java FrameWorks
    • Logback
      • 自定义 logback 日志过滤器
  • Java FrameWorks
    • Mybatis
      • MyBatis(十三) - 整合Spring
  • Java FrameWorks
    • Mybatis
      • MyBatis(十二) - 一些API
  • Java FrameWorks
    • Mybatis
      • Mybatis(一) - 概述
  • Java FrameWorks
    • Mybatis
      • Mybatis(七) - 结果集的封装与映射
  • Java FrameWorks
    • Mybatis
      • Mybatis(三) - mapper.xml及其加载机制
  • Java FrameWorks
    • Mybatis
      • Mybatis(九) - 事务
  • Java FrameWorks
    • Mybatis
      • Mybatis(二) - 全局配置文件及其加载机制
  • Java FrameWorks
    • Mybatis
      • Mybatis(五) - SqlSession执行流程
  • Java FrameWorks
    • Mybatis
      • Mybatis(八) - 缓存
  • Java FrameWorks
    • Mybatis
      • Mybatis(六) - 动态SQL的参数绑定与执行
  • Java FrameWorks
    • Mybatis
      • Mybatis(十) - 插件
  • Java FrameWorks
    • Mybatis
      • Mybatis(十一) - 日志
  • Java FrameWorks
    • Mybatis
      • Mybatis(四) - Mapper接口解析
  • Java FrameWorks
    • Netty
      • Netty 可靠性分析
  • Java FrameWorks
    • Netty
      • Netty - Netty 线程模型
  • Java FrameWorks
    • Netty
      • Netty堆外内存泄露排查盛宴
  • Java FrameWorks
    • Netty
      • Netty高级 - 高性能之道
  • Java FrameWorks
    • Shiro
      • Shiro + JWT + Spring Boot Restful 简易教程
  • Java FrameWorks
    • Shiro
      • 非常详尽的 Shiro 架构解析!
  • Java FrameWorks
    • Spring
      • Spring AOP 使用介绍,从前世到今生
  • Java FrameWorks
    • Spring
      • Spring AOP 源码解析
  • Java FrameWorks
    • Spring
      • Spring Event 实现原理
  • Java FrameWorks
    • Spring
      • Spring Events
  • Java FrameWorks
    • Spring
      • Spring IOC容器源码分析
  • Java FrameWorks
    • Spring
      • Spring Integration简介
  • Java FrameWorks
    • Spring
      • Spring MVC 框架中拦截器 Interceptor 的使用方法
  • Java FrameWorks
    • Spring
      • Spring bean 解析、注册、实例化流程源码剖析
  • Java FrameWorks
    • Spring
      • Spring validation中@NotNull、@NotEmpty、@NotBlank的区别
  • Java FrameWorks
    • Spring
      • Spring 如何解决循环依赖?
  • Java FrameWorks
    • Spring
      • Spring 异步实现原理与实战分享
  • Java FrameWorks
    • Spring
      • Spring中的“for update”问题
  • Java FrameWorks
    • Spring
      • Spring中的设计模式
  • Java FrameWorks
    • Spring
      • Spring事务失效的 8 大原因
  • Java FrameWorks
    • Spring
      • Spring事务管理详解
  • Java FrameWorks
    • Spring
      • Spring计时器StopWatch使用
  • Java FrameWorks
    • Spring
      • 详述 Spring MVC 框架中拦截器 Interceptor 的使用方法
  • Java FrameWorks
    • Spring
      • 透彻的掌握 Spring 中@transactional 的使用
  • Java
    • Java IO&NIO&AIO
      • Java IO - BIO 详解
  • Java
    • Java IO&NIO&AIO
      • Java NIO - IO多路复用详解
  • Java
    • Java IO&NIO&AIO
      • Java N(A)IO - Netty
  • Java
    • Java IO&NIO&AIO
      • Java IO - Unix IO模型
  • Java
    • Java IO&NIO&AIO
      • Java IO - 分类
  • Java
    • Java IO&NIO&AIO
      • Java NIO - 基础详解
  • Java
    • Java IO&NIO&AIO
      • Java IO - 常见类使用
  • Java
    • Java IO&NIO&AIO
      • Java AIO - 异步IO详解
  • Java
    • Java IO&NIO&AIO
      • Java IO概述
  • Java
    • Java IO&NIO&AIO
      • Java IO - 设计模式
  • Java
    • Java IO&NIO&AIO
      • Java NIO - 零拷贝实现
  • Java
    • Java JVM
      • JVM 优化经验总结
  • Java
    • Java JVM
      • JVM 内存结构
  • Java
    • Java JVM
      • JVM参数设置
  • Java
    • Java JVM
      • Java 内存模型
  • Java
    • Java JVM
      • 从实际案例聊聊Java应用的GC优化
  • Java
    • Java JVM
      • Java 垃圾回收器G1详解
  • Java
    • Java JVM
      • 垃圾回收器Shenandoah GC详解
  • Java
    • Java JVM
      • 垃圾回收器ZGC详解
  • Java
    • Java JVM
      • 垃圾回收基础
  • Java
    • Java JVM
      • 如何优化Java GC
  • Java
    • Java JVM
      • 类加载机制
  • Java
    • Java JVM
      • 类字节码详解
  • Java
    • Java 基础
      • Java hashCode() 和 equals()
  • Java
    • Java 基础
      • Java 基础 - Java native方法以及JNI实践
  • Java
    • Java 基础
      • Java serialVersionUID 有什么作用?
  • Java
    • Java 基础
      • Java 泛型的类型擦除
  • Java
    • Java 基础
      • Java 基础 - Unsafe类解析
  • Java
    • Java 基础
      • Difference Between Statement and PreparedStatement
  • Java
    • Java 基础
      • Java 基础 - SPI机制详解
  • Java
    • Java 基础
      • Java 基础 - final
  • Java
    • Java 基础
      • Java中static关键字详解
  • Java
    • Java 基础
      • 为什么说Java中只有值传递?
  • Java
    • Java 基础
      • Java 基础 - 即时编译器原理解析及实践
  • Java
    • Java 基础
      • Java 基础 - 反射
  • Java
    • Java 基础
      • Java多态的面试题
  • Java
    • Java 基础
      • Java 基础 - 异常机制详解
  • Java
    • Java 基础
      • 为什么要有抽象类?
  • Java
    • Java 基础
      • 接口的本质
  • Java
    • Java 基础
      • Java 基础 - 枚举
  • Java
    • Java 基础
      • Java 基础 - 泛型机制详解
  • Java
    • Java 基础
      • Java 基础 - 注解机制详解
  • Java
    • Java 基础
      • 为什么 String hashCode 方法选择数字31作为乘子
  • Java
    • Java 并发
      • Java 并发 - 14个Java并发容器
  • Java
    • Java 并发
      • Java 并发 - AQS
  • Java
    • Java 并发
      • Java 并发 - BlockingQueue
  • Java
    • Java 并发
      • Java 并发 - CAS
  • Java
    • Java 并发
      • Java 并发 - Condition接口
  • Java
    • Java 并发
      • Java 并发 - CopyOnWriteArrayList
  • Java
    • Java 并发
      • Java 并发 - CountDownLatch、CyclicBarrier和Phaser对比
  • Java
    • Java 并发
      • Java 并发 - Fork&Join框架
  • Java
    • Java 并发
      • Java 并发 - Java CompletableFuture 详解
  • Java
    • Java 并发
      • Java 并发 - Java 线程池
  • Java
    • Java 并发
      • Java 并发 - Lock接口
  • Java
    • Java 并发
      • Java 并发 - ReentrantLock
  • Java
    • Java 并发
      • Java 并发 - ReentrantReadWriteLock
  • Java
    • Java 并发
      • Java 并发 - Synchronized
  • Java
    • Java 并发
      • Java 并发 - ThreadLocal 内存泄漏问题
  • Java
    • Java 并发
      • Java 并发 - ThreadLocal
  • Java
    • Java 并发
      • Java 并发 - Volatile
  • Java
    • Java 并发
      • Java 并发 - 从ReentrantLock的实现看AQS的原理及应用
  • Java
    • Java 并发
      • Java 并发 - 公平锁和非公平锁
  • Java
    • Java 并发
      • Java 并发 - 内存模型
  • Java
    • Java 并发
      • Java 并发 - 原子类
  • Java
    • Java 并发
      • Java 并发 - 如何确保三个线程顺序执行?
  • Java
    • Java 并发
      • Java 并发 - 锁
  • Java
    • Java 的新特性
      • Java 10 新特性概述
  • Java
    • Java 的新特性
      • Java 11 新特性概述
  • Java
    • Java 的新特性
      • Java 12 新特性概述
  • Java
    • Java 的新特性
      • Java 13 新特性概述
  • Java
    • Java 的新特性
      • Java 14 新特性概述
  • Java
    • Java 的新特性
      • Java 15 新特性概述
  • Java
    • Java 的新特性
      • Java 8的新特性
  • Java
    • Java 的新特性
      • Java 9 新特性概述
  • Java
    • Java 调试排错
      • 调试排错 - Java Debug Interface(JDI)详解
  • Java
    • Java 调试排错
      • 调试排错 - CPU 100% 排查优化实践
  • Java
    • Java 调试排错
      • 调试排错 - Java Heap Dump分析
  • Java
    • Java 调试排错
      • 调试排错 - Java Thread Dump分析
  • Java
    • Java 调试排错
      • 调试排错 - Java动态调试技术原理
  • Java
    • Java 调试排错
      • 调试排错 - Java应用在线调试Arthas
  • Java
    • Java 调试排错
      • 调试排错 - Java问题排查:工具单
  • Java
    • Java 调试排错
      • 调试排错 - 内存溢出与内存泄漏
  • Java
    • Java 调试排错
      • 调试排错 - 在线分析GC日志的网站GCeasy
  • Java
    • Java 调试排错
      • 调试排错 - 常见的GC问题分析与解决
  • Java
    • Java 集合
      • Java 集合 - ArrayList
  • Java
    • Java 集合
      • Java 集合 - HashMap 和 ConcurrentHashMap
  • Java
    • Java 集合
      • Java 集合 - HashMap的死循环问题
  • Java
    • Java 集合
      • Java 集合 - LinkedHashSet&Map
  • Java
    • Java 集合
      • Java 集合 - LinkedList
  • Java
    • Java 集合
      • Java 集合 - PriorityQueue
  • Java
    • Java 集合
      • Java 集合 - Stack & Queue
  • Java
    • Java 集合
      • Java 集合 - TreeSet & TreeMap
  • Java
    • Java 集合
      • Java 集合 - WeakHashMap
  • Java
    • Java 集合
      • Java 集合 - 为什么HashMap的容量是2的幂次方
  • Java
    • Java 集合
      • Java 集合 - 概览
  • Java
    • Java 集合
      • Java 集合 - 高性能队列Disruptor详解
  • 分布式
    • RPC
      • ⭐️RPC - Dubbo&hsf&Spring cloud的区别
  • 分布式
    • RPC
      • ⭐️RPC - Dubbo的架构原理
  • 分布式
    • RPC
      • ⭐️RPC - HSF的原理分析
  • 分布式
    • RPC
      • ⭐️RPC - 你应该知道的RPC原理
  • 分布式
    • RPC
      • ⭐️RPC - 动态代理
  • 分布式
    • RPC
      • 深入理解 RPC 之协议篇
  • 分布式
    • RPC
      • RPC - 序列化和反序列化
  • 分布式
    • RPC
      • ⭐️RPC - 服务注册与发现
  • 分布式
    • RPC
      • RPC - 核心原理
  • 分布式
    • RPC
      • ⭐️RPC - 框架对比
  • 分布式
    • RPC
      • ⭐️RPC - 网络通信
  • 分布式
    • 分布式事务
      • 分布式事务 Seata TCC 模式深度解析
  • 分布式
    • 分布式事务
      • 分布式事务的实现原理
  • 分布式
    • 分布式事务
      • 常用的分布式事务解决方案
  • 分布式
    • 分布式事务
      • 手写实现基于消息队列的分布式事务框架
  • 分布式
    • 分布式算法
      • CAP 定理的含义
  • 分布式
    • 分布式算法
      • Paxos和Raft比较
  • 分布式
    • 分布式算法
      • 分布式一致性与共识算法
  • 分布式
    • 分布式锁
      • ⭐️分布式锁的原理及实现方式
  • 分布式
    • 搜索引擎
      • ElasticSearch与SpringBoot的集成与JPA方法的使用
  • 分布式
    • 搜索引擎
      • 全文搜索引擎 Elasticsearch 入门教程
  • 分布式
    • 搜索引擎
      • 十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统
  • 分布式
    • 搜索引擎
      • 腾讯万亿级 Elasticsearch 技术解密
  • 分布式
    • 日志系统
      • Grafana Loki 简明教程
  • 分布式
    • 日志系统
      • 分布式系统中如何优雅地追踪日志
  • 分布式
    • 日志系统
      • 如何优雅地记录操作日志?
  • 分布式
    • 日志系统
      • 日志收集组件—Flume、Logstash、Filebeat对比
  • 分布式
    • 日志系统
      • 集中式日志系统 ELK 协议栈详解
  • 分布式
    • 消息队列
      • 消息队列 - Kafka
  • 分布式
    • 消息队列
      • 消息队列 - Kafka、RabbitMQ、RocketMQ等消息中间件的对比
  • 分布式
    • 消息队列
      • 消息队列之 RabbitMQ
  • 分布式
    • 消息队列
      • 消息队列 - 使用docker-compose构建kafka集群
  • 分布式
    • 消息队列
      • 消息队列 - 分布式系统与消息的投递
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息的可靠性传输
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息的顺序性
  • 分布式
    • 消息队列
      • 消息队列 - 如何保证消息队列的高可用
  • 分布式
    • 消息队列
      • 消息队列 - 消息队列设计精要
  • 分布式
    • 监控系统
      • 深度剖析开源分布式监控CAT
  • 大数据
    • Flink
      • Flink架构与核心组件
  • 微服务
    • Dubbo
      • 基于dubbo的分布式应用中的统一异常处理
  • 微服务
    • Dubbo
      • Vim快捷键
  • 微服务
    • Service Mesh
      • Istio 是什么?
  • 微服务
    • Service Mesh
      • OCTO 2.0:美团基于Service Mesh的服务治理系统详解
  • 微服务
    • Service Mesh
      • Service Mesh是什么?
  • 微服务
    • Service Mesh
      • Spring Cloud向Service Mesh迁移
  • 微服务
    • Service Mesh
      • 数据挖掘算法
  • 微服务
    • Service Mesh
      • Seata Saga 模式
  • 微服务
    • Spring Cloud
      • Seata TCC 模式
  • 微服务
    • Spring Cloud
      • Spring Cloud Config
  • 微服务
    • Spring Cloud
      • Seata AT 模式
  • 微服务
    • Spring Cloud
      • Spring Cloud Gateway
  • 微服务
    • Spring Cloud
      • Spring Cloud OpenFeign 的核心原理
  • 微服务
    • Spring Cloud
      • Seata XA 模式
  • 数据库
    • Database Version Control
      • Liquibase vs. Flyway
  • 数据库
    • Database Version Control
      • Six reasons to version control your database
  • 数据库
    • MySQL
      • How Sharding Works
  • 数据库
    • MySQL
      • MySQL InnoDB中各种SQL语句加锁分析
  • 数据库
    • MySQL
      • MySQL 事务隔离级别和锁
  • 数据库
    • MySQL
      • MySQL 索引性能分析概要
  • 数据库
    • MySQL
      • MySQL 索引设计概要
  • 数据库
    • MySQL
      • MySQL出现Waiting for table metadata lock的原因以及解决方法
  • 数据库
    • MySQL
      • MySQL的Limit性能问题
  • 数据库
    • MySQL
      • MySQL索引优化explain
  • 数据库
    • MySQL
      • MySQL索引背后的数据结构及算法原理
  • 数据库
    • MySQL
      • MySQL行转列、列转行问题
  • 数据库
    • MySQL
      • 一条SQL更新语句是如何执行的?
  • 数据库
    • MySQL
      • 一条SQL查询语句是如何执行的?
  • 数据库
    • MySQL
      • 为什么 MySQL 使用 B+ 树
  • 数据库
    • MySQL
      • 为什么 MySQL 的自增主键不单调也不连续
  • 数据库
    • MySQL
      • 为什么我的MySQL会“抖”一下?
  • 数据库
    • MySQL
      • 为什么数据库不应该使用外键
  • 数据库
    • MySQL
      • 为什么数据库会丢失数据
  • 数据库
    • MySQL
      • 事务的可重复读的能力是怎么实现的?
  • 数据库
    • MySQL
      • 大众点评订单系统分库分表实践
  • 数据库
    • MySQL
      • 如何保证缓存与数据库双写时的数据一致性?
  • 数据库
    • MySQL
      • 浅谈数据库并发控制 - 锁和 MVCC
  • 数据库
    • MySQL
      • 深入浅出MySQL 中事务的实现
  • 数据库
    • MySQL
      • 浅入浅出MySQL 和 InnoDB
  • 数据库
    • PostgreSQL
      • PostgreSQL upsert功能(insert on conflict do)的用法
  • 数据库
    • Redis
      • Redis GEO & 实现原理深度分析
  • 数据库
    • Redis
      • Redis 和 I/O 多路复用
  • 数据库
    • Redis
      • Redis分布式锁
  • 数据库
    • Redis
      • Redis实现分布式锁中的“坑”
  • 数据库
    • Redis
      • Redis总结
  • 数据库
    • Redis
      • 史上最全Redis高可用技术解决方案大全
  • 数据库
    • Redis
      • Redlock:Redis分布式锁最牛逼的实现
  • 数据库
    • Redis
      • 为什么 Redis 选择单线程模型
  • 数据库
    • TiDB
      • 新一代数据库TiDB在美团的实践
  • 数据库
    • 数据仓库
      • 实时数仓在有赞的实践
  • 数据库
    • 数据库原理
      • OLTP与OLAP的关系是什么?
  • 数据库
    • 数据库原理
      • 为什么 OLAP 需要列式存储
  • 系统设计
    • DDD
      • Domain Primitive
  • 系统设计
    • DDD
      • Repository模式
  • 系统设计
    • DDD
      • 应用架构
  • 系统设计
    • DDD
      • 聊聊如何避免写流水账代码
  • 系统设计
    • DDD
      • 领域层设计规范
  • 系统设计
    • DDD
      • 从三明治到六边形
  • 系统设计
    • DDD
      • 阿里盒马领域驱动设计实践
  • 系统设计
    • DDD
      • 领域驱动设计(DDD)编码实践
  • 系统设计
    • DDD
      • 领域驱动设计在互联网业务开发中的实践
  • 系统设计
    • 基础架构
      • 容错,高可用和灾备
  • 系统设计
    • 数据聚合
      • GraphQL及元数据驱动架构在后端BFF中的实践
  • 系统设计
    • 数据聚合
      • 高效研发-闲鱼在数据聚合上的探索与实践
  • 系统设计
    • 服务安全
      • JSON Web Token 入门教程
  • 系统设计
    • 服务安全
      • 你还在用JWT做身份认证嘛?
  • 系统设计
    • 服务安全
      • 凭证(Credentials)
  • 系统设计
    • 服务安全
      • 授权(Authorization)
  • 系统设计
    • 服务安全
      • 理解OAuth2.0
  • 系统设计
    • 服务安全
      • 认证(Authentication)
  • 系统设计
    • 架构案例
      • 微信 Android 客户端架构演进之路
  • 系统设计
    • 高可用架构
      • 业务高可用的保障:异地多活架构
  • 计算机基础
    • 字符编码
      • Base64原理解析
  • 计算机基础
    • 字符编码
      • 字符编码笔记:ASCII,Unicode 和 UTF-8
  • 计算机基础
    • 操作系统
      • 为什么 CPU 访问硬盘很慢
  • 计算机基础
    • 操作系统
      • 为什么 HTTPS 需要 7 次握手以及 9 倍时延
  • 计算机基础
    • 操作系统
      • 为什么 Linux 默认页大小是 4KB
  • 计算机基础
    • 操作系统
      • 磁盘IO那些事
  • 计算机基础
    • 操作系统
      • 虚拟机的3种网络模式
  • 计算机基础
    • 服务器
      • mac终端bash、zsh、oh-my-zsh最实用教程
  • 计算机基础
    • 服务器
      • Nginx强制跳转Https
  • 计算机基础
    • 服务器
      • curl 的用法指南
  • 计算机基础
    • 网络安全
      • 如何设计一个安全的对外接口?
  • 计算机基础
    • 网络安全
      • 浅谈常见的七种加密算法及实现
  • 计算机基础
    • 网络编程
      • MQTT - The Standard for IoT Messaging
  • 计算机基础
    • 网络编程
      • 两万字长文 50+ 张趣图带你领悟网络编程的内功心法
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有 TIME_WAIT 状态
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有性能问题
  • 计算机基础
    • 网络编程
      • 为什么 TCP 协议有粘包问题
  • 计算机基础
    • 网络编程
      • 为什么 TCP 建立连接需要三次握手
  • 计算机基础
    • 网络编程
      • 为什么 TCP/IP 协议会拆分数据
  • 计算机基础
    • 网络编程
      • 使用 OAuth 2 和 JWT 为微服务提供安全保障
  • 计算机基础
    • 网络编程
      • 四种常见的 POST 提交数据方式
  • 计算机基础
    • 网络编程
      • 有赞TCP网络编程最佳实践
  • 计算机基础
    • 网络编程
      • 看完这篇HTTP,跟面试官扯皮就没问题了
  • 计算机基础
    • 网络编程
      • 详细解析 HTTP 与 HTTPS 的区别
  • 质量&效率
    • 快捷键
      • Idea快捷键(Mac版)
  • 质量&效率
    • 快捷键
      • Shell快捷键
  • 质量&效率
    • 快捷键
      • conduit
  • 质量&效率
    • 敏捷开发
      • Scrum的3种角色
  • 质量&效率
    • 敏捷开发
      • Scrum的4种会议
  • 质量&效率
    • 敏捷开发
      • ThoughtWorks的敏捷开发
  • 质量&效率
    • 敏捷开发
      • 敏捷开发入门教程
  • 运维&测试
    • Docker
      • Docker (容器) 的原理
  • 运维&测试
    • Docker
      • Docker Compose:链接外部容器的几种方式
  • 运维&测试
    • Docker
      • Docker 入门教程
  • 运维&测试
    • Docker
      • Docker 核心技术与实现原理
  • 运维&测试
    • Docker
      • Dockerfile 最佳实践
  • 运维&测试
    • Docker
      • Docker开启Remote API 访问 2375端口
  • 运维&测试
    • Docker
      • Watchtower - 自动更新 Docker 镜像与容器
  • 运维&测试
    • Kubernetes
      • Kubernetes 介绍
  • 运维&测试
    • Kubernetes
      • Kubernetes 在有赞的实践
  • 运维&测试
    • Kubernetes
      • Kubernetes 学习路径
  • 运维&测试
    • Kubernetes
      • Kubernetes如何改变美团的云基础设施?
  • 运维&测试
    • Kubernetes
      • Kubernetes的三种外部访问方式:NodePort、LoadBalancer 和 Ingress
  • 运维&测试
    • Kubernetes
      • 谈 Kubernetes 的架构设计与实现原理
  • 运维&测试
    • 压测
      • 全链路压测平台(Quake)在美团中的实践
  • 运维&测试
    • 测试
      • Cpress - JavaScript End to End Testing Framework
  • 运维&测试
    • 测试
      • 代码覆盖率-JaCoCo
  • 运维&测试
    • 测试
      • 浅谈代码覆盖率
  • 运维&测试
    • 测试
      • 测试中 Fakes、Mocks 以及 Stubs 概念明晰
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP中的Bean是如何被AOP代理的
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP原生动态代理和Cglib动态代理
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP实现方式(xml&注解)
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP是如何收集切面类并封装的
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP概述
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP的底层核心后置处理器
  • Java FrameWorks
    • Spring
      • Spring AOP
        • Spring AOP的延伸知识
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(一)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(三)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(二)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(五)
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - IOC(四) - 循环依赖与解决方案
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot - 启动引导
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot JarLauncher
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot Web Mvc 自动装配
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 使用ApplicationListener监听器
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 声明式事务
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot 嵌入式容器
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot引起的“堆外内存泄漏”排查及经验总结
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot的启动流程
  • Java FrameWorks
    • Spring
      • Spring Boot
        • Spring Boot自动化配置源码分析
  • Java FrameWorks
    • Spring
      • Spring Boot
        • 如何自定义Spring Boot Starter?
  • Java FrameWorks
    • Spring
      • Spring IOC
        • IOC - 模块装配和条件装配
  • Java FrameWorks
    • Spring
      • Spring IOC
        • IOC - 配置源(xml,注解)
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Environment
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring ApplicationContext
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanDefinition
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanFactory
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanFactoryPostProcessor
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring BeanPostProcessor
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(一) - 概述
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(三) - 实例化阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(二) - BeanDefinition
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(五) - 销毁阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Bean的生命周期(四) - 初始化阶段
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring ComponentScan
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Events
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 基础篇
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 总结
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC 进阶篇
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring IOC容器的生命周期
  • Java FrameWorks
    • Spring
      • Spring IOC
        • Spring Resource
  • Java FrameWorks
    • Spring
      • Spring MVC
        • DispatcherServlet的初始化原理
  • Java FrameWorks
    • Spring
      • Spring MVC
        • DispatcherServlet的核心工作原理
  • Java FrameWorks
    • Spring
      • Spring MVC
        • WebMvc的架构设计与组件功能解析
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Security Oauth
  • Java FrameWorks
    • Spring
      • Spring Security
        • Spring Security
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • DispatcherHandler的工作原理(传统方式)
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • DispatcherHandler的工作原理(函数式端点)
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • WebFlux的自动装配
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • 快速了解响应式编程与Reactive
  • Java FrameWorks
    • Spring
      • Spring WebFlux
        • 快速使用WebFlux
  • 分布式
    • 协调服务
      • Zookeeper
        • Zookeeper - 客户端之 Curator
  • 分布式
    • 协调服务
      • Zookeeper
        • 详解分布式协调服务 ZooKeeper
  • 分布式
    • 协调服务
      • etcd
        • 高可用分布式存储 etcd 的实现原理
  • 数据库
    • Database Version Control
      • Flyway
        • Database Migrations with Flyway
  • 数据库
    • Database Version Control
      • Flyway
        • How Flyway works
  • 数据库
    • Database Version Control
      • Flyway
        • Rolling Back Migrations with Flyway
  • 数据库
    • Database Version Control
      • Flyway
        • The meaning of the concept of checksums
  • 数据库
    • Database Version Control
      • Liquibase
        • Introduction to Liquibase Rollback
  • 数据库
    • Database Version Control
      • Liquibase
        • LiquiBase中文学习指南
  • 数据库
    • Database Version Control
      • Liquibase
        • Use Liquibase to Safely Evolve Your Database Schema
  • 系统设计
    • 流量控制
      • RateLimiter
        • Guava Rate Limiter实现分析
  • 系统设计
    • 流量控制
      • Sentinel
        • Sentinel 与 Hystrix 的对比
  • 系统设计
    • 流量控制
      • Sentinel
        • Sentinel工作主流程
  • 系统设计
    • 流量控制
      • 算法
        • 分布式服务限流实战
  • 系统设计
    • 解决方案
      • 秒杀系统
        • 如何设计一个秒杀系统
  • 系统设计
    • 解决方案
      • 红包系统
        • 微信高并发资金交易系统设计方案--百亿红包背后的技术支撑
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 什么是预排序遍历树算法(MPTT,Modified Preorder Tree Traversal)
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 加密算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 推荐系统算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • linkerd
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 查找算法
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 缓存淘汰算法中的LRU和LFU
  • 计算机基础
    • 数据结构与算法
      • 其他相关
        • 负载均衡算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Paxos算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Raft算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - Snowflake算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - ZAB算法
  • 计算机基础
    • 数据结构与算法
      • 分布式算法
        • 分布式算法 - 一致性Hash算法
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Bitmap & Bloom Filter
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Map & Reduce
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - Trie树/数据库/倒排索引
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 分治/hash/排序
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 双层桶划分
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 外(磁盘文件)排序
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理 - 布隆过滤器
  • 计算机基础
    • 数据结构与算法
      • 大数据处理
        • 大数据处理算法
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 文本预处理:后缀树(Suffix Tree)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:BM 算法 (Boyer-Moore)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:KMP 算法(Knuth-Morris-Pratt)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配 - 模式预处理:朴素算法(Naive)(暴力破解)
  • 计算机基础
    • 数据结构与算法
      • 字符串匹配算法
        • 字符串匹配
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 分支限界算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 分治算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 动态规划算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 回溯算法
  • 计算机基础
    • 数据结构与算法
      • 常用算法
        • 贪心算法
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 十大排序算法
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(一)之3种简单排序(选择,冒泡,直接插入)
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(三)之堆排序
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(二)之希尔排序
  • 计算机基础
    • 数据结构与算法
      • 排序算法
        • 图解排序算法(四)之归并排序
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 树的高度和深度
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 红黑树深入剖析及Java实现
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 线性结构 - Hash
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 线性结构 - 数组、链表、栈、队列
  • 计算机基础
    • 数据结构与算法
      • 数据结构
        • 逻辑结构 - 树
  • 运维&测试
    • 测试
      • Spock
        • Groovy 简明教程
  • 运维&测试
    • 测试
      • Spock
        • Spock 官方文档
  • 运维&测试
    • 测试
      • Spock
        • Spock单元测试框架介绍以及在美团优选的实践
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(一)
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(三)
  • 运维&测试
    • 测试
      • TDD
        • TDD 实践 - FizzFuzzWhizz(二)
  • 运维&测试
    • 测试
      • TDD
        • 测试驱动开发(TDD)- 原理篇
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Nacos
          • Nacos 服务注册的原理
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Nacos
          • Nacos 配置中心原理分析
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • 服务调用过程
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Bus
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Consul
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Seata
          • Spring Cloud Stream
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Sentinel
          • Sentinel 与 Hystrix 的对比
  • 微服务
    • Spring Cloud
      • Spring Cloud Alibaba
        • Sentinel
          • Sentinel
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • How Hystrix Works
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Hystrix
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Hystrix原理与实战
  • 微服务
    • Spring Cloud
      • Spring Cloud Netflix
        • Hystrix
          • Spring Cloud Hystrix基本原理
由 GitBook 提供支持
在本页
  • 1. 一级缓存
  • 1.1 一级缓存的使用
  • 1.2 一级缓存的设计原理
  • 2. 二级缓存
  • 2.1 二级缓存的使用
  • 2.2 二级缓存的设计原理

这有帮助吗?

  1. Java FrameWorks
  2. Mybatis

Mybatis(八) - 缓存

1. 一级缓存

1.1 一级缓存的使用

一级缓存的使用相当的简单,MyBatis 本身就开启一级缓存(通常我们也不会控制它关闭),所以我们可以直接拿来用。

1.1.1 简单使用

一级缓存基于 SqlSession ,所以我们可以直接创建 SqlSessionFactory ,并从中开启一个新的 SqlSession ,默认情况下它会自动开启事务,所以一级缓存会自动使用。

下面我们编写一个简单的示例,这里面我们调用两次 DepartmentMapper 的 findAll 方法,由于使用一级缓存,所以第二次 findAll 方法会直接使用一级缓存的数据,而不会再次向数据库发送 SQL 语句:

public class Level1Application {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("第一次执行findAll......");
        departmentMapper.findAll();
        System.out.println("第二次执行findAll......");
        departmentMapper.findAll();
    
        sqlSession.close();
    }
}

执行 main 方法,观察控制台的打印,会发现第一次执行 findAll 方法时,它会开启一个 jdbc 的连接,并且发送 SQL 语句到数据库,但第二次再调用时,它没有再次发送 SQL :(控制台没有打印,完事后直接关闭 jdbc 连接了)

第一次执行findAll......
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG source.pooled.PooledDataSource  - Created connection 2130772866. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
第二次执行findAll......
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG source.pooled.PooledDataSource  - Returned connection 2130772866 to pool.

这就是一级缓存最基本的使用。

1.1.2 清空一级缓存

前面在第 8 章中,我们讲到 statement 的定义里面,flushCache 这个属性时提到过,它可以清空一级缓存和它所属的 namespace 下的二级缓存,当清空后,再次调用 findAll 时 MyBatis 就会重新发送 SQL 到数据库执行查询了。

一个简单的示例如下:

    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("第一次执行findAll......");
        departmentMapper.findAll();
        System.out.println("第二次执行findAll......");
        departmentMapper.findAll();
        System.out.println("清空一级缓存......");
        departmentMapper.cleanCache();
        System.out.println("清空缓存后再次执行findAll......");
        departmentMapper.findAll();
    
        sqlSession.close();
    }

以此法编写的 main 方法运行结果如下:

第一次执行findAll......
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
第二次执行findAll......
清空一级缓存......
[main] DEBUG er.DepartmentMapper.cleanCache  - ==>  Preparing: select count(id) from tbl_department 
[main] DEBUG er.DepartmentMapper.cleanCache  - ==> Parameters:  
[main] DEBUG er.DepartmentMapper.cleanCache  - <==      Total: 1 
清空缓存后再次执行findAll......
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4

可见,清空一级缓存后,findAll 方法又重新发送 SQL 查询数据库了。

1.1.3 一级缓存失效的情景

虽说一级缓存确实很好,不过由于一些使用不当,或者意外情况,一级缓存会失效,失效的表现肯定是重复发送同样的 SQL 了。下面我们就来看看,哪些情况会导致一级缓存的失效,以及无法使用到一级缓存的情况。

1.1.3.1 跨SqlSession的一级缓存不共享

这个很好理解,一级缓存本身就是 SqlSession 级别的缓存,这些缓存只在本 SqlSession 内有效,不同的 SqlSession 一级缓存不共享。下面我们可以来验证一下。(下面的示例来自 Level1InvalidApplication )

    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        
        // 跨SqlSession的一级缓存不共享
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
        
        departmentMapper.findAll();
        departmentMapper2.findAll();
        
        sqlSession.close();
        sqlSession2.close();
    }

看,这样开启两个 SqlSession ,在执行 findAll 查询时,观察控制台的日志打印,会发现开启了两个全新的 jdbc Connection ,并且也发送了两次相同的 SQL 。

[main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG source.pooled.PooledDataSource  - Created connection 2130772866. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG source.pooled.PooledDataSource  - Created connection 1861781750. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6ef888f6] 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4

1.1.3.2 两次相同的查询间有DML操作

DML 操作,也就是增删改了,我们前面学习第 8 章时都知道,insert 、update 、delete 标签的 flushCache 默认为 true ,执行它们时,必然会导致一级缓存的清空,从而引发之前的一级缓存不能继续使用。这个效果的演示,小册就不重复演示了,与上面 1.2 节的效果是完全一样的。

1.1.3.3 手动清空了一级缓存

可能有的小伙伴注意到了,SqlSession 有一个 clearCache 方法,调用它会直接清空一级缓存,非常简单有效 ~ 下面我们也来试一下效果。

DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
departmentMapper.findAll();
System.out.println("重复调用findAll方法......");
departmentMapper.findAll();
System.out.println("手动清空SqlSession的缓存......");
sqlSession.clearCache();
System.out.println("清空缓存后重新调用findAll方法......");
departmentMapper.findAll();
System.out.println("--------------------------------");

这样编写好后,第二次重复调用 findAll 方法时,控制台不会发送新的 SQL 语句,但是手动清空后,再调用,控制台就可以看到 SQL 语句的打印了:

重复调用findAll方法......
手动清空SqlSession的缓存......
清空缓存后重新调用findAll方法......
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
--------------------------------

1.1.3.4 与Spring整合时没有开启事务

默认情况下我们拿到的 SqlSession 都是开启了事务的,即便是在用 SqlSessionFactory 获取 SqlSession 时,传入的参数为 true ( sqlSessionFactory.openSession(true) ,意味着不开启事务),连续查询两次 findAll 方法时一级缓存也会生效。不过可能有的小伙伴遇到过一个特殊的情况:用 SpringFramework / SpringBoot 整合 MyBatis 时,当 Service 的方法没有标注 **@Transactional** 注解,或者没有被事务增强器的通知切入时,两次查询同一条数据时,会发送两次 SQL 到数据库,这样看上去像是一级缓存失效了!这种情况出现的原因,小册在这里作一个补充性的讲解。

SpringFramework / SpringBoot 整合 MyBatis 后,Service 方法中没有开启事务时,每次调用 Mapper 查询数据时,底层都会创建一个全新的 **SqlSession** 去查数据库,而一级缓存本身就是基于 SqlSession 的,每次都开启全新的,那不就相当于上面的 1.3.1 节提到的,跨 SqlSession 的一级缓存不共享了嘛。

1.1.4 使用一级缓存要注意的

一级缓存固然好用,但小心一个比较危险的东西:一级缓存是存放到 SqlSession 中,如果我们在查询到数据后,直接在数据对象上作修改,修改之后又重新查询相同的数据,虽然此时一级缓存可以生效,但因为存放的数据其实是对象的引用,导致第二次从一级缓存中查询到的数据,就是我们刚刚改过的数据,这样可能会发生一些错误。

可能这样只用文字说不好理解,我们配一段代码演示一下:

public class Level1ReferenceApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("department: " + department);
        department.setName("哈哈哈哈");
        System.out.println("department: " + department);
        
        Department department2 = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("department2: " + department2);
        System.out.println(department == department2);
    }
}

看这段代码,上面先查询一次 id 为 18ec781fbefd727923b0d35740b177ab 的部门,查询出来以后打印一下,随后将其 name 改为 "哈哈哈哈" ,修改后再次查询 id 为 18ec781fbefd727923b0d35740b177ab 的部门,由于此时一级缓存生效,会把缓存中的数据拿出来,最后我们对比一下两个 Department 对象的引用是否一致(即判断两个对象是否为同一个)。

运行 main 方法,控制台的打印结果如下:

department: Department{id='18ec781fbefd727923b0d35740b177ab', name='开发部', tel='123'}
department: Department{id='18ec781fbefd727923b0d35740b177ab', name='哈哈哈哈', tel='123'}
department2: Department{id='18ec781fbefd727923b0d35740b177ab', name='哈哈哈哈', tel='123'}
true

危险的现象出现了:由于我们修改了第一次查询的结果,而这个结果本身就是一级缓存中存放的数据库查询结果,导致我们修改了其中的 name 属性后,第二次再查询时,取出来的数据是我们刚刚修改了的!这样就有可能引发一些不必要的麻烦和错误了。

如何避免这种情况呢?本质的目的是为了将之前的一级缓存失效掉。要么,用全新的 SqlSession ,要么,查询前清一下一级缓存。不过上面提到的那篇文章中还提到了另外一种方案:全局配置中,设置 **local-cache-scope** 属性为 **statement** ,不过这种设置的方法是针对全局了,不是很合适,所以小册在此不展开,有关 localCacheScope 配置的描述,小伙伴们可以参照 MyBatis 的文档描述(即下图)。

1.2 一级缓存的设计原理

掌握了一级缓存的使用以及要注意的,下面我们深入源码中,探究一下一级缓存的设计,以及在查询中如何发挥其作用的。

1.2.1 缓存模型的设计

首先我们先来了解一下 MyBatis 的缓存模型,其实缓存的本质是一个类似于 **Map** 的东西,有 key 有 value 。MyBatis 中专门设计了一个 **Cache** 接口来模仿 Map ,定义缓存最基本的增删改查方法。

1.2.1.1 Cache接口与实现类

public interface Cache {
    // 每个缓存都有id
    String getId();
    // 放缓存
    void putObject(Object key, Object value);
    // 取缓存
    Object getObject(Object key);
    // 删缓存
    Object removeObject(Object key);
    // 清缓存
    void clear();
    // 查大小
    int getSize();
}

有接口那就一定有实现,借助 IDEA ,可以发现 Cache 接口的实现类还是很多的:

注意观察包名!绝大多数 Cache 实现类的包名,最后有个 decorators ,这很明显是装饰者的意思呀!只有一个 PerpetualCache ,包名的最后是 impl ,那得了,这分明就是一个实现类 + 好多个装饰者的设计了。可是为什么 MyBatis 要将缓存模型设计为一堆装饰者呢?

1.2.1.2 Cache实现类的装饰者设计意义

究其根源,我们要先提一点 MyBatis 二级缓存的东西了,MyBatis 中的二级缓存本身是占用应用空间的,换句话说,MyBatis 中的二级缓存实际使用的是 JVM 的内存,那默认情况来讲,占用的内存太多不利于应用本身的正常运行,所以 MyBatis 会针对缓存的各种特性和过期策略,设计了一些能够修饰原本缓存件的装饰者,以此达到动态拼装缓存实现的目的。

这个设计到后面的设计模式章节还会再展开讲,毕竟现在我们还没有讲到二级缓存,可能小伙伴们没概念,下一章我们把二级缓存过完之后,在回过头来,可能理解起来会更容易一点。

1.2.1.3 PerpetualCache的设计

既然大部分都是装饰者,那我们先看看这个本身 Cache 接口的最初实现 PerpetualCache ,它是一个没有任何修饰的、最单纯的缓存实现:

public class PerpetualCache implements Cache {

    private final String id;

    private final Map<Object, Object> cache = new HashMap<>();

噗。。。这设计也忒不走心了吧,直接套一个 HashMap 就完事了?哎,还真就套一层就完事了!因为缓存本身就是 Map 类型的设计,直接拿现成的岂不美哉?

至于其它的嘛,现在一级缓存阶段暂时涉及不到,小册先不展开了,小伙伴们也不要在这个阶段去翻看,避免本末倒置。

1.2.2 一级缓存的设计位置

既然有了 PerpetualCache ,那它一定是组合到某个位置,从而形成一级缓存的吧!小册先不讲,小伙伴们来猜一下,这个 PerpetualCache 能放在哪里呢?

emmmmm,大概率是 SqlSession 的实现类中吧!我们翻开 SqlSession 接口的默认实现 DefaultSqlSession :

public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;

    private final boolean autoCommit;
    private boolean dirty;
    private List<Cursor<?>> cursorList;
}

很抱歉 ~ 这里并没有缓存的设计呢 ~ (手动狗头)

哎,但是小伙伴们不要急,观察一下这几个成员,你觉得 PerpetualCache 最有可能放在这里面的谁里头呢?

连想都不用想,肯定是 Executor ,Configuration 本身是全局的配置,不适合放 SqlSession 实例相关的东西,下面 3 个很明显都放不下,那只剩下 Executor 了。OK ,下面我们进去看看 Executor 吧:

public interface Executor { ... }

呃。。。又是接口。。。这让我咋找呢?诶,别急,我们继续往下看实现类就是啦!借助 IDEA ,可以发现 Executor 有如下几个实现类:

这看类名,第一直觉肯定是 CachingExecutor 吧!这类名上都明摆着写着缓存了!我们赶紧点进去看看:

public class CachingExecutor implements Executor {

    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}

emmmmmm。。。搁这逗我呢?这里面咋是套了一层代理呢?到底真正干活的是谁?

好吧,其实最底层的还是要看 BaseExecutor ,这个类名的设计比较类似于 AbstractXXX ,它本身也是一个抽象类,是它里面组合了 PerpetualCache :

public abstract class BaseExecutor implements Executor {

    private static final Log log = LogFactory.getLog(BaseExecutor.class);

    protected Transaction transaction;
    protected Executor wrapper;

    protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
    // 【!!!看这里!!!】
    protected PerpetualCache localCache;
    protected PerpetualCache localOutputParameterCache;
    protected Configuration configuration;
}

可能小伙伴读到这里会有一些许的不爽,“明明一开始直接告诉我不就得了?为什么非要跟耍猴似的折腾我呢?”

小伙伴们不要生气不要着急,阿熊我本身一开始翻这部分源码的时候,就是这个心路历程,很多小伙伴想知道我平时读源码的思路,觉得我在小册里没有讲解很多,所以阿熊也在想着如何能把我的一些思路通过比较容易接受的方式,跟小伙伴们分享一下。思来想去,这样似乎是不错的,于是乎这里就设计了这样的一个你们可能感觉有些 “耍猴” 的环节,希望小伙伴们能理解啦。

OK ,了解了一级缓存的设计位置,下面我们再来看看,如果一个 select 查询被执行时,一级缓存是如何工作的。

1.2.3 一级缓存的生效原理

我们以上面 1.1 的简单示例来测试,在此之前我们找到 BaseExecutor 这个类的 query 方法( MyBatis 3.5.5 版本在第 141 行),在这个方法体的第 152 行打一个断点,然后我们以 Debug 的方式运行 Level1Application ,当程序停在断点时,我们观察一下一级缓存是如何生效和工作的。

1.2.3.1 query方法概览

我们先看看 query 方法的核心逻辑吧,先大概有个思路:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ......
    // 如果statement指定了需要刷新缓存,则清空一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 查询之前先检查一级缓存中是否存在数据
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 有,则直接取缓存
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 没有,则查询数据库
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        // ......
        // 全局localCacheScope设置为statement,则清空一级缓存
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    return list;
}

可以发现,一级缓存起作用的位置,是在向数据库发起查询之前,先拦截检查一下,如果一级缓存中有数据,则直接从缓存中取数据并返回,否则才查询数据库。

理清楚思路后,我们来 Debug 运行。

1.2.3.2 第一次进入BaseExecutor

list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
  handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
  list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

第一次执行 departmentMapper.findAll() 方法,此时可以发现 localCache 中是空的,一级缓存干干净净:

OK ,没有数据,那就必须走数据库查询了,进入 queryFromDatabase 方法:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                                      ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 缓存占位,代表此时还没有查询到数据
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 执行数据库查询
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    // 查询结果放入缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

可以发现,queryFromDatabase 方法中主要干的事情,就是查询数据,并放入缓存了。由于查询到了结果,放入了缓存,所以返回到外层的 query 方法后,localCache 中就有数据了:

1.2.3.3 第二次进入BaseExecutor

第二次执行 departmentMapper.findAll() 方法,因为此时缓存中已经有数据了,所以上面的判断会走 if 的分支而不是 else :

这个方法就没啥意思了,主要是对存储过程有输出资源的一些处理,我们不关心,重要的是,list 这个变量有值了,query 方法的最底下就是返回 list 这个变量,所以第二次查询走一级缓存这个特性就得以体现了。

1.2.3.4 清空一级缓存

执行了两次 findAll 方法后,接下来要清空一级缓存了,还记得 query 方法一开始的源码吗:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ......
    // 如果statement指定了需要刷新缓存,则清空一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    // ......
}

如果 statement 指定了需要刷新缓存,则一级缓存会被清空。而 cleanCache 对应的 mapper.xml 中就是指定了 flushCache=true :

<select id="cleanCache" resultType="int" flushCache="true">
    select count(id) from tbl_department
</select>

那这里 Debug 的时候,借助 IDEA 可以发现此处返回 true :

clearLocalCache 方法被执行,一级缓存也就清空了。

OK ,以上就是一级缓存的生效和工作原理了,整体来看比较简单易懂,小伙伴们可以跟着源码 Debug 走一下,体会一下一级缓存的设计。

2. 二级缓存

2.1 二级缓存的使用

2.1.1 简单使用

最简单的使用方式,只需要在 mapper.xml 上打一个 <cache /> 标签,就算开启二级缓存了:

<?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.linkedbear.mybatis.mapper.DepartmentMapper">
    <cache />
    <!-- ...... -->
</mapper>

对应的 Mapper 接口,则需要用 @CacheNamespace 注解开启:

@CacheNamespace
public interface DepartmentMapper {
    // ......
}

另外,不要忘记给实体类实现 Serializable 接口,否则二级缓存也是不能用的。

然后我们就可以编写测试代码了,这个测试代码完全可以基于之前的一级缓存测试代码扩展。我们知道,使用二级缓存时,必须关闭 SqlSession 时,一级缓存的数据才会写入二级缓存,所以此处我们需要在查询动作完成后,关闭 sqlSession ,并重新开启一个新的 SqlSession :

public class Level2Application {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("第一次执行findAll......");
        departmentMapper.findAll();
        System.out.println("第二次执行findAll......");
        departmentMapper.findAll();
        
        sqlSession.close();
    
        // 开启一个新的SqlSession,测试二级缓存
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
        System.out.println("sqlSession2执行findAll......");
        departmentMapper2.findAll();
        
        sqlSession2.close();
    }
}

编写好测试代码后,运行 main 方法,观察控制台的 SQL 日志打印:

第一次执行findAll......
[main] DEBUG .mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.0 
[main] DEBUG ion.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG .pooled.PooledDataSource  - Created connection 2061347276. 
[main] DEBUG ion.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection] 
[main] DEBUG DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG DepartmentMapper.findAll  - <==      Total: 4 
第二次执行findAll......
[main] DEBUG .mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.0 
[main] DEBUG ion.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection] 
[main] DEBUG ion.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7adda9cc] 
[main] DEBUG .pooled.PooledDataSource  - Returned connection 2061347276 to pool. 
sqlSession2执行findAll......
[main] DEBUG .mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.3333333333333333

sqlSession 第一次执行 findAll 方法时,由于一级缓存和二级缓存中都没有数据,所以需要查询数据库,查询到数据后第二次执行 findAll 方法时,一级缓存中已经存在数据,所以无需再查询数据库。关闭 sqlSession 时,可以发现控制台中有 Closing JDBC Connection 的字眼,此时一级缓存中的数据已经保存至二级缓存。另外开启 sqlSession2 时,再执行 findAll 方法,控制台甚至连 Connection 都懒得打开了,因为 MyBatis 发现二级缓存中有现成的数据了,于是直接取出,返回。

从这段过程中,我们可以总结出几个细节:

  • SqlSession 关闭时,一级缓存的数据进入二级缓存。

  • 二级缓存中有数据时,直接取出,不会预先开启 Connection (按需加载的思想)

1.1.2 二级缓存的配置

默认的二级缓存开启,其实背后都有一些 MyBatis 帮我们设定好的默认值,我们可以通过修改这些配置,达到自定义本地二级缓存的目的。

修改的载体必然是这个 <cache> 标签(对应 Mapper 接口的则是 @CacheNamespace 注解的属性),它有不少属性,下面我们先看看它都可以配置什么。

属性
描述
备注

eviction

缓存的回收策略

默认 LRU

type

二级缓存的实现

默认 org.apache.ibatis.cache.impl.PerpetualCache ,即本地内存的二级缓存

size

缓存引用数量

默认值 1024

flushInterval

缓存刷新间隔(定时清除时间间隔)

默认无,即没有刷新间隔

readOnly

缓存是否只读

默认 false ,需要二级缓存对应的实体模型类需要实现 Serializable 接口

blocking

阻塞获取缓存数据

若缓存中找不到对应的 key ,是否会一直 blocking ,直到有对应的数据进入缓存。默认 false

属性不算多,但我们接触的不是很多,下面小册就一些比较重要的属性展开讲解一下。

1.1.2.1 eviction

缓存的回收策略,它可以配置当缓存容量即将溢出时如何回收空间。MyBatis 的官方文档中提到的可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。

  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。

  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认情况下我们不需要多余配置,默认的 LRU 策略已经足够用。

1.1.2.2 type

缓存的实现载体,上一章我们在了解缓存模型中就知道,MyBatis 的缓存根接口是 Cache ,那有接口就要有实现类,默认的实现类与一级缓存一样,都是 **PerpetualCache** ,如果我们需要使用外置第三方缓存件,那这个 type 属性就需要指定了(比方说整合 ehcache 的 org.mybatis.caches.ehcache.EhcacheCache )。如何整合 EhCache ,我们下面马上就会讲到。

1.1.2.3 readOnly

缓存是否只读,这个设置比较有趣。还记得上一章讲过的一级缓存吗,我们当时测试了一个场景,如果第一次查出数据后,直接修改该数据,之后第二次查询时,从一级缓存中查出来的数据是被修改过的,并非数据库的真实数据,原因是 MyBatis 利用一级缓存是直接将数据的引用交出去,至于我们怎么利用,MyBatis 不管。

二级缓存就不一样了,我们从二级缓存中查出来的数据那可是跨 SqlSession 的,谁知道你改不改数据(还不敢保证改的对不对),万一你改了那别人从二级缓存中拿的数据就是被你改过的,这样万一出点问题,那可就出大事了。MyBatis 自然帮我们考虑到了这一点,于是它给二级缓存设计了一个只读属性。这个只读属性如果设置为 false ,则通过二级缓存查询的数据会执行一次基于 jdk 序列化的对象深拷贝,这样就可以保证拿到的数据不会对原二级缓存产生影响(但一次对象的深拷贝会导致性能降低);而 readOnly 设置为 true ,则只读的缓存会像一级缓存那样,直接返回二级缓存本身,虽然可能不安全,但好在处理速度快。

由此也就解释了,为什么默认情况下,开启 MyBatis 的二级缓存,需要实体模型类实现 Serializable 接口。

1.1.3 整合EhCache

OK ,下面我们来回顾一下 MyBatis 如何整合外置第三方缓存,比较流行的缓存是 EhCache ,最近几年也有 MyBatis 整合 Redis 做二级缓存的了,整合的逻辑都是一样的。下面我们以整合 EhCache 为例回顾。

1.1.3.1 导入依赖

既然整合 EhCache ,那 jar 包必然少不了了,MyBatis 本身已经提供了整合的 jar 包,所以直接拿过来用就可以:

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-ehcache</artifactId>
        <version>1.2.1</version>
    </dependency>

1.1.3.2 配置EhCache

接下来,我们需要配置一下 EhCache 了,EhCache 的配置,需要在 src/main/resources 中放一个 ehcache.xml 的配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    <!--
        磁盘存储:将缓存中暂时不使用的对象,转移到硬盘,类似于Windows系统的虚拟内存
        path:指定在硬盘上存储对象的路径
     -->
    <diskStore path="C:\ehcache"/>

    <!--
        defaultCache:默认的缓存配置信息,如果不加特殊说明,则所有对象按照此配置项处理
        maxElementsInMemory:设置内存缓存的上限,最多存储多少个记录对象
        maxElementsOnDisk:设置硬盘缓存的上限,内存放不下时会向硬盘中缓存(0表示无上限)
        eternal:代表对象是否永不过期
        timeToIdleSeconds:最大的空闲时间(秒)(对象在多长时间没有被访问就会失效)
        timeToLiveSeconds:最大的存活时间(秒)(对象从创建到失效所需要的时间)
        overflowToDisk:是否允许对象被写入到磁盘
        memoryStoreEvictionPolicy:缓存清空策略
            * FIFO:先进先出
            * LFU:最少使用的清空
            * LRU:最近最少使用(即未被使用的时间最长)
     -->
    <defaultCache
            maxElementsInMemory="100"
            maxElementsOnDisk="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
    />
</ehcache>

这个配置文件不需要我们全部写,直接抄过来就 OK 。相关的配置在上面也有注释,小伙伴们可以根据自己的需要合理配置。

还有一点,不要忘记,哪里需要用二级缓存,哪里就配置上 EhCache 的缓存实现:

<mapper namespace="com.linkedbear.mybatis.mapper.DepartmentMapper">
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    <!-- ...... -->
</mapper>

1.1.3.3 测试效果

不需要任何多余的编写测试代码,我们直接重新运行一遍 Level2Application 即可。运行结束后,我们观察一下 C 盘有没有多一个 ehcache 文件夹,以及里面有没有缓存文件生成:

可以发现,数据已经成功的借助 EhCache 缓存到磁盘上了,说明 EhCache 整合成功。

到这里,对于 MyBatis 的二级缓存也就讲解完毕了,接下来,又到了大家最“头疼”的原理分析环节。

2.2 二级缓存的设计原理

首先我们先回顾一下二级缓存的模型设计,以及里面涉及到的装饰者模式。

2.2.1 Cache实现类的装饰者模式

上一章我们提到了 Cache 接口的那些实现类:

这里面只有 PerpetualCache 是真正有缓存能力的实现类,其余的都是装饰者。装饰者最大的特征,是在原有的功能上扩展新的特性,多种装饰者的组合,可以保证任意增加新的功能行为而不用修改原有的基本代码。

读到这里,小伙伴们肯定会产生一个新的疑问:**MyBatis 怎么根据我们的二级缓存的配置,构造对应的缓存实现呢?**哎这个问题到位了,下面我们要填之前的坑了。

2.2.2 二级缓存的初始化位置

我们之前提到了 MyBatis 解析 mapper.xml 和 Mapper 接口时,会处理 <cache> 标签和 @CacheNamespace 注解,当时我们把这部分跳过了,本章我们就回过头来看看那部分源码的实现。

2.2.2.1 mapper.xml中的cache解析

还记得解析 mapper.xml 的位置起源吧,是解析 MyBatis 的全局配置文件,里面会解析 <mapper> 标签,从而触发 mapper.xml 的解析,而解析 mapper.xml 的逻辑中有如下两行源码:

cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));

上面是处理 <cache-ref> 的,我们暂且不关心,下面的 cacheElement 方法是处理 <cache> 标签。

2.2.2.2 解析cache标签

进入 cacheElement 方法:(关键注释已标注在源码中)

private void cacheElement(XNode context) {
    if (context != null) {
        // 默认的类型是PERPETUAL,也即PerpetualCache
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 默认的过期策略 LRU
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        // 获取其他属性
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        // 2.2.3 创建Cache对象
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

从源码中可以很明显的发现,缓存实现类、过期策略都可以声明别名的,说明它们的底层其实都是对应的某些实现类,而这些实现类,早在第 7 章中我们就看过了,可能部分小伙伴忘记了它的位置,小册提醒一下,是在 BaseBuilder 的构造方法中:

public abstract class BaseBuilder {
    protected final Configuration configuration;
    protected final TypeAliasRegistry typeAliasRegistry;
    protected final TypeHandlerRegistry typeHandlerRegistry;

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        // 注意这个typeAliasRegistry
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    }

注意这个 typeAliasRegistry ,这里面注册了 MyBatis 默认的一些内置别名,其中就有如下这样一段:

    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

可以发现,这些别名对应的都是 Cache 接口的实现类!由此也能在一定程度上透漏着装饰者的味道了吧。

OK ,我们的重点不在这里,更为重要的是关心最下面的 builderAssistant.useNewCache 方法。

2.2.2.3 创建Cache对象

又用到 MapperBuilderAssistant 了,这家伙真有点 “无所不能” 的意思啊,我们看看它如何创建出 Cache 对象的吧:

public Cache useNewCache(Class<? extends Cache> typeClass, 
        Class<? extends Cache> evictionClass,
        Long flushInterval, Integer size, boolean readWrite,
        boolean blocking, Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
                          .implementation(valueOrDefault(typeClass, PerpetualCache.class))
                          .addDecorator(valueOrDefault(evictionClass, LruCache.class))
                          .clearInterval(flushInterval)
                          .size(size)
                          .readWrite(readWrite)
                          .blocking(blocking)
                          .properties(props)
                          .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

注意看!它使用了一个 CacheBuilder 创建的 Cache 对象!这很明显是建造器的设计。仔细观察一下这段链式调用,可以发现 <cache> 标签中的属性,在这里全部都用到了,先不点进去看,光在外头,应该各位会产生一种强烈的感觉:每一行调用都有可能外挂一个装饰者!到底是不是这样呢?我们分解来看。

CacheBuilder的成员

我们先看看 CacheBuilder 本身的设计:

public class CacheBuilder {
    private final String id;
    private Class<? extends Cache> implementation;
    private final List<Class<? extends Cache>> decorators;
    private Integer size;
    private Long clearInterval;
    private boolean readWrite;
    private Properties properties;
    private boolean blocking;
}

可以发现,这里面也是包含了 <cache> 标签的所有必备要素,这里面两个小细节:

  • implementation 属性对应的是 Cache 接口的落地实现,decorators 代表要外挂的装饰者们;

  • properties 属性意味着 <cache> 标签也有 <property> 子标签,可以传入配置。

了解了基本设计,下面我们就挑上面重要的方法来看。

implementation

注意看 implementation(valueOrDefault(typeClass, PerpetualCache.class)) 这行代码,它的入参是一个 Class 对象,而且带默认值 PerpetualCache ,这很明显是为了确定 Cache 接口的落地实现,在没有任何整合的前提下,MyBatis 肯定会用 PerpetualCache 作为落地实现。

addDecorator

看方法名,就差扒拉着耳朵告诉你它要加装饰者了,这个方法的实现,就是向 decorators 这个集合中添加装饰者实现:

private final List<Class<? extends Cache>> decorators;

public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
    if (decorator != null) {
        this.decorators.add(decorator);
    }
    return this;
}

不过默认情况下,它只会添加一个 LruCache 的实现,难不成它意味着默认创建出来的缓存只有一层装饰者吗?带着这个疑问,我们继续往下看。

readWrite

剩下几个方法中,小册选了一个比较简单且有代表性的方法来看:

public CacheBuilder readWrite(boolean readWrite) {
    this.readWrite = readWrite;
    return this;
}

看上去它只是把 “缓存是否只读” 记录到 CacheBuilder 中而已,没啥别的意思?但实际上不是这么简单(有伏笔),我们继续往下看。

build

最下面的动作是构建二级缓存了:(关键注释已标注在源码中)

public Cache build() {
    // 兜底处理
    setDefaultImplementations();
    // 创建默认的PerpetualCache对象
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    // 如果是PerpetualCache类,则用装饰者逐层包装
    if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 2.2.3.5 包装完毕后,处理MyBatis的标准装饰者
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}

纵读整段源码,其实都不算难理解了,装饰者模式在此体现得淋漓尽致。

源码的中间偏下位置,在所有传入的装饰者都包装完成后,还有一个 setStandardDecorators 方法,它就是前面提到的伏笔了。

setStandardDecorators

进入 setStandardDecorators 方法中,我们在此又发现了好多装饰者:(注释已标全)

private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        // 缓存大小
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        // 定时清空二级缓存
        if (clearInterval != null) {
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        // 读写缓存
        if (readWrite) {
            cache = new SerializedCache(cache);
        }
        // 外挂日志记录、同步缓存
        cache = new LoggingCache(cache);
        cache = new SynchronizedCache(cache);
        // 阻塞读取缓存
        if (blocking) {
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}

确实是伏笔的回应吧,上面记录的这些属性,在下面都有对应的装饰者 Cache ,所以最终这些配置都是以装饰者的身份,包装到最底层的 PerpetualCache 上了。

经过这一系列逻辑处理后,Cache 对象也就成功的创建了,二级缓存也就初始化完成了。

2.2.3 二级缓存的生效原理

接下来是二级缓存的生效机制探究了,我们还是以 1.1 节的最简单的二级缓存使用作为测试代码,调试观察二级缓存的生效过程。

2.2.3.1 准备断点

跟上一章一样,我们先准备断点。这次我们把断点打在 org.apache.ibatis.executor.CachingExecutor 的第 96 行,即 query 方法的第 2 行:

private final TransactionalCacheManager tcm = new TransactionalCacheManager();

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

我们可以顺便看一下这个 query 方法的大体逻辑。一上来它会先去尝试获取二级缓存,如果没有二级缓存,则直接执行查询(跳到 BaseExecutor 中了);如果有二级缓存,则会尝试从 TransactionalCacheManager 中拿着二级缓存和缓存的 key 取查询数据,如果获取到了,则直接返回,没有获取到,则查询到结果后放入二级缓存中。

2.2.3.2 第一次进入断点

之后我们就可以以 Debug 的方式运行 Level2Application 了,当程序停在断点时,我们观察二级缓存是如何生效和工作的。

断点落下,此时可以发现 cache 是有值的,而且确实是按照上面构建的方式一步一步套出来的:

然后,它要使用 tcm.getObject(cache, key) 方法,从二级缓存中取数据,显然刚开始运行,二级缓存是空的,所以必然返回的 list 为 null :

既然没有数据,那就查数据库咯,查询完成后使用 tcm.putObject(cache, key, list); 放入二级缓存。

不过请注意,这个 putObject 方法是不会直接放入二级缓存的,我们可以通过 Debug 的断点处查看数据:

为什么缓存为空呢?我们要看一下 putObject 方法的实现:

// TransactionalCacheManager
public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}

// TransactionCache
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

注意最下面它 put 的那个集合变量名:entriesToAddOnCommit ,OnCommit ???合着要等到事务提交咯?那当然啦,我们在学习 MyBatis 二级缓存的时候就知道,MyBatis 的二级缓存需要在 **SqlSession** 关闭时,一级缓存中的数据才能写入二级缓存,这里当然不能直接存进去。

这样经过一轮查询后,SqlSession 的一级缓存中就已经有数据了,第二次再进入断点时依然是上面的流程,不再重复。

2.2.3.3 SqlSession关闭

SqlSession 关闭时,一级缓存的数据要写入二级缓存,此时会触发 Executor 的关闭,我们找到 CachingExecutor 的 close 方法:(可能会有小伙伴不理解为什么又找到 Executor 而不是 SqlSession ,我们后面放到生命周期部分再详细讲解)

public void close(boolean forceRollback) {
    try {
        // issues #499, #524 and #573
        if (forceRollback) {
            tcm.rollback();
        } else {
            tcm.commit();
        }
    } finally {
        delegate.close(forceRollback);
    }
}

注意看,它又调用了 TransactionalCacheManager 的 commit 方法。看到这里可能有部分小伙伴的脑子里问号越来越多了:搞缓存就搞缓存嘛,为啥非要扯上事务呢?

TransactionalCacheManager的设计缘由

仔细思考一下,二级缓存是跨 SqlSession 的,也就是跨 Connection 的,那既然是跨连接,就必须要考虑到事务了,否则会出现一些意外情况,比方说小册来举个例子:

如果二级缓存的存放不需要考虑事务的话,那就有可能出现上面的问题:sqlSession1 先更新数据,后查询全部,此时查询出来的数据是修改后的脏数据,就这样直接放入二级缓存了,但随后 sqlSession1 执行了 rollback ,撤消了修改的数据,但数据库里的数据可以撤销修改,但二级缓存没办法撤销呀,这样就造成了隐患。sqlSession1 关闭后,重新开启一个新的 SqlSession ,并直接查询数据,此时二级缓存中有被修改过的错误数据,但 sqlSession2 并不知情,导致就这么把错误数据取出来了,从而引发错误。

由此我们就应该清楚,二级缓存应该是基于事务提交的,只有事务提交后,数据库的数据确定没有问题,这个时候 **SqlSession** 中的一级缓存数据也是准确的,这样才能把一级缓存的数据写入到二级缓存中,这也就是 TransactionalCacheManager 设计的意义。

commit的动作

搞明白 TransactionalCacheManager 的良苦用心,那我们就看看 commit 的动作中都干了什么吧:

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
        txCache.commit();
    }
}

呦,这是把它里面的所有缓存都取出来,挨个 commit 呀,刚才上面我们也看到了,TransactionalCache 里面有那个 entriesToAddOnCommit 集合,那是不是 commit 了之后,相应的这些集合的数据也就都写入到二级缓存呢?答案是肯定的,进入到 TransactionalCache 中,源码的逻辑非常简单:

public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    // 刷新等待写入的缓存
    flushPendingEntries();
    reset();
}

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        // 写入最底层的缓存中
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

可以发现,它还是一层装饰者,最里层的 delegate 肯定还是 PerpetualCache ,commit 的动作就是将 entriesToAddOnCommit 中的数据写入最内层的二级缓存中。

相应的,如果是回滚的话,只需要把这些集合全部清空即可:

public void rollback() {
    unlockMissedEntries();
    reset();
}

private void reset() {
    clearOnCommit = false;
    // 直接清空所有要写入的缓存
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
}

经过这样一番操作之后,一级缓存的数据就写入到二级缓存中了。

2.2.3.4 第三次进入断点

第三次进入断点,此时 sqlSession 已经关闭,sqlSession2 开启后进入。此时只凭观察 Cache 对象的属性,就已经知道二级缓存真实的存在了:

注意看 HashMap 中的 value ,是一个字节数组,这也就说明了二级缓存在写入时已经执行了一次基于 jdk 的序列化动作,每次从二级缓存取数据时,会再执行一次反序列化,将字节数组转为缓存数据对象。

至此,我们也就对整个二级缓存的生效原理也研究明白了。

上一页Mybatis下一页Java FrameWorks

最后更新于2年前

这有帮助吗?

有关这部分原理,小伙伴们可以参照我之前写过的一篇文章理解:

img
img
img
img
img
img
img

MyBatis 的官方文档中也有对 EhCache 整合的说明: 。

img
img
img
img
img
img
img
MyBatis的一级缓存竟然还会引来麻烦?
mybatis.org/ehcache-cac…