MyBatis缓存机制与注解开发前言上一篇我们梳理了MyBatis关联查询的核心实现本篇继续深入MyBatis进阶知识点缓存机制和注解开发。缓存是提升数据库查询性能的关键手段而注解开发则提供了更轻量的代码编写方式。本文结合实战代码从原理到实践全面拆解这两个核心模块。一、MyBatis缓存机制详解1. 缓存基础概念什么是缓存缓存是存储在内存中的临时数据将用户高频查询的数据保存在内存中下次查询时直接从内存获取避免频繁访问磁盘数据库从而大幅提升查询效率解决高并发系统的性能瓶颈。为什么使用缓存减少与数据库的交互次数降低数据库IO压力减少系统开销提升响应速度提升系统并发处理能力缓存适用数据经常查询且不常改变的数据如系统配置信息字典表数据用户基础信息非实时更新统计报表数据按周期更新不适用缓存的数据实时性要求极高的数据如股票价格频繁更新的数据如订单状态敏感数据如用户密码2. 一级缓存SqlSession级本地缓存工作原理一级缓存是MyBatis默认开启的本地缓存作用域为单个SqlSession。在同一次SqlSession会话中执行相同的查询语句MyBatis会将第一次查询结果存入内存缓存后续相同查询直接从缓存中获取不再访问数据库底层实现使用PerpetualCache基于HashMap存储数据一级缓存测试TestpublicvoidfindById()throwsIOException{// 加载配置文件创建SqlSessioninResources.getResourceAsStream(SqlMapConfig.xml);SqlSessionFactoryfactorynewSqlSessionFactoryBuilder().build(in);sessionfactory.openSession();mappersession.getMapper(UserDao.class);// 第一次查询访问数据库Useruser1mapper.findById(1);System.out.println(user1);System.out.println(-----------------);// 第二次查询从一级缓存获取Useruser2mapper.findById(1);System.out.println(user2);// 验证两个对象是同一个引用System.out.println(user1user2);// 输出truesession.close();in.close();}执行日志分析只打印了一次SQL语句第二次查询没有访问数据库直接从缓存获取user1 user2为true说明缓存中存储的是对象引用一级缓存失效的4种场景一级缓存的生命周期与SqlSession绑定以下情况会导致缓存失效失效场景说明SqlSession不同每个SqlSession有自己独立的一级缓存不同会话的缓存互不影响SqlSession相同查询条件不同缓存是基于SQL语句和参数的不同查询条件会生成不同的缓存keySqlSession相同两次查询之间执行了增删改操作增删改操作会清空当前SqlSession的所有一级缓存避免脏数据SqlSession相同手动清除缓存调用session.clearCache()方法手动清空一级缓存示例增删改导致缓存失效TestpublicvoidtestCacheInvalid(){Useruser1mapper.findById(1);System.out.println(user1);// 执行删除操作清空一级缓存mapper.delete(2);session.commit();// 第二次查询重新访问数据库Useruser2mapper.findById(1);System.out.println(user2);System.out.println(user1user2);// 输出false}3. 二级缓存SqlSessionFactory级全局缓存工作原理二级缓存是跨SqlSession的全局缓存作用域为同一个namespaceMapper接口。多个SqlSession共享同一个namespace的二级缓存当SqlSession关闭或提交时会将一级缓存的数据写入二级缓存后续其他SqlSession执行相同查询时直接从二级缓存获取二级缓存存储的是数据不是对象引用因此返回的是对象的拷贝二级缓存开启条件二级缓存需要手动开启满足以下4个条件全局配置开启二级缓存默认已开启建议显式配置settingssettingnamecacheEnabledvaluetrue//settingsMapper映射文件中声明使用二级缓存!-- 在UserMapper.xml中添加 --cache/实体类实现Serializable接口publicclassUserimplementsSerializable{// 实体类属性和方法}原因二级缓存可能会将数据序列化到磁盘因此需要实体类支持序列化SqlSession必须关闭或提交只有当SqlSession关闭或提交后一级缓存的数据才会被写入二级缓存二级缓存测试TestpublicvoidtestSecondLevelCache()throwsIOException{InputStreaminResources.getResourceAsStream(SqlMapConfig.xml);SqlSessionFactoryfactorynewSqlSessionFactoryBuilder().build(in);// 第一个SqlSessionSqlSessionsession1factory.openSession();UserDaomapper1session1.getMapper(UserDao.class);Useruser1mapper1.findById(1);System.out.println(user1);session1.close();// 关闭session1将数据写入二级缓存System.out.println(-----------------);// 第二个SqlSessionSqlSessionsession2factory.openSession();UserDaomapper2session2.getMapper(UserDao.class);Useruser2mapper2.findById(1);System.out.println(user2);session2.close();// 验证二级缓存存储的是数据不是对象引用System.out.println(user1user2);// 输出falsein.close();}执行日志分析只打印了一次SQL语句日志中出现Cache Hit Ratio [com.qcby.dao.UserDao]: 0.5表示二级缓存命中率为50%user1 user2为false说明返回的是不同的对象实例二级缓存失效场景两次查询之间执行了任意增删改操作会同时清空一级和二级缓存不同namespace下的增删改操作不会影响其他namespace的二级缓存cache标签参数详解可以通过配置cache标签的属性自定义二级缓存的行为cacheevictionFIFO!--缓存回收策略--flushInterval60000!-- 缓存刷新间隔单位毫秒 --size512!-- 缓存最大对象数 --readOnlytrue/!-- 是否只读 --参数说明可选值eviction缓存回收策略LRU最近最少使用默认、FIFO先进先出、SOFT软引用、WEAK弱引用flushInterval缓存刷新间隔任意正整数毫秒默认不设置仅在调用语句时刷新size缓存最大对象数任意正整数默认1024readOnly是否只读true返回缓存对象的相同实例性能高不安全false返回缓存对象的拷贝安全性能低默认4. MyBatis缓存查询顺序MyBatis执行查询时会按照以下顺序查找数据先查二级缓存因为二级缓存中可能有其他SqlSession查询过的数据二级缓存未命中再查一级缓存一级缓存也未命中查询数据库SqlSession关闭后将一级缓存的数据写入二级缓存5. 自定义缓存Ehcache简介MyBatis提供了缓存接口Cache可以通过实现该接口自定义缓存。常用的第三方缓存实现是Ehcache它是一个纯Java的进程内缓存框架支持内存和磁盘存储。使用步骤引入Ehcache依赖编写Ehcache配置文件在Mapper映射文件中指定缓存实现类cachetypeorg.mybatis.caches.ehcache.EhcacheCache/6. 缓存使用的核心注意事项脏数据问题二级缓存是namespace级别的当多个Mapper操作同一张表时可能会出现脏数据。建议单表操作使用二级缓存多表联查避免使用。分布式环境MyBatis的二级缓存是进程内缓存分布式环境下会出现数据不一致问题。分布式系统建议使用Redis等分布式缓存。缓存穿透查询不存在的数据时会直接访问数据库。可以通过缓存空值或布隆过滤器解决。缓存雪崩大量缓存同时过期导致数据库压力骤增。可以通过设置不同的过期时间解决。二、MyBatis注解开发实战1. 注解开发概述MyBatis支持通过注解方式编写SQL语句无需编写XML映射文件简化了开发流程。但注解开发也有局限性复杂的动态SQL和关联查询不如XML灵活。优缺点对比优点缺点代码简洁无需编写XML文件复杂动态SQL难以维护与Java代码集成度高便于调试关联查询配置繁琐简单CRUD开发效率高SQL语句硬编码在Java代码中修改需要重新编译适合快速开发和小型项目不适合大型复杂项目核心注解总览注解作用Select执行查询操作Insert执行插入操作Update执行更新操作Delete执行删除操作Results定义结果集映射Result单个字段映射ResultMap引用已定义的结果集映射SelectKey插入后获取自增主键One多对一/一对一关联查询Many一对多关联查询2. 基础CRUD注解实现核心配置在SqlMapConfig.xml中配置Mapper扫描mappers!-- 扫描指定包下的所有Mapper接口 --packagenamecom.qcby.dao//mappers增删改查注解示例publicinterfaceUserAnnoDao{// 查询所有用户Select(select * from user)Results(iduserMap,value{Result(propertyid,columnid),Result(propertyusername,columnusername),Result(propertybirthday,columnbirthday),Result(propertysex,columnsex),Result(propertyaddress,columnaddress)})ListUserfindAll();// 根据ID查询用户Select(select * from user where id #{id})ResultMap(userMap)// 复用上面定义的结果集映射UserfindById(intid);// 添加用户Insert(insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address}))SelectKey(statementselect last_insert_id(),// 获取自增主键的SQLkeyColumnid,// 数据库主键列名keyPropertyid,// 实体类主键属性名beforefalse,// 是否在插入前执行resultTypeInteger.class// 返回值类型)intinsert(Useruser);// 更新用户Update(update user set username #{username}, birthday #{birthday}, sex #{sex}, address #{address} where id #{id})intupdate(Useruser);// 删除用户Delete(delete from user where id #{id})intdelete(intid);// 查询用户总数Select(select count(*) from user)intfindCount();// 模糊查询Select(select * from user where username like concat(%, #{username}, %))ListUserfindByName(Stringusername);}测试代码publicclassUserAnnoTest{privateInputStreamin;privateSqlSessionsession;privateUserAnnoDaomapper;Beforepublicvoidinit()throwsIOException{inResources.getResourceAsStream(SqlMapConfig.xml);SqlSessionFactoryfactorynewSqlSessionFactoryBuilder().build(in);sessionfactory.openSession();mappersession.getMapper(UserAnnoDao.class);}Afterpublicvoiddestroy()throwsIOException{session.commit();// 提交事务session.close();in.close();}TestpublicvoidtestFindAll(){ListUserusersmapper.findAll();for(Useruser:users){System.out.println(user);}}TestpublicvoidtestInsert(){UserusernewUser();user.setUsername(小美);user.setSex(女);user.setBirthday(newDate());user.setAddress(保定);introwsmapper.insert(user);System.out.println(插入行数rows);System.out.println(自增主键user.getId());// 通过SelectKey获取}}3. 关联查询注解实现多对一查询学生→老师方式1立即加载联表查询publicinterfaceStudentAnnoDao{Select(SELECT student.*, teacher.Tname FROM student LEFT JOIN teacher ON student.t_id teacher.id)Results({Result(propertyid,columnid),Result(propertyname,columnname),Result(propertysex,columnsex),Result(propertyage,columnage),Result(propertyt_id,columnt_id),Result(propertyteacher.Tname,columnTname)})ListStudentfindAllWithTeacher();}方式2延迟加载分步查询publicinterfaceStudentAnnoDao{Select(select * from student)Results({Result(propertyid,columnid),Result(propertyname,columnname),Result(propertysex,columnsex),Result(propertyage,columnage),Result(propertyt_id,columnt_id),// 多对一关联One注解Result(propertyteacher,columnt_id,oneOne(selectcom.qcby.dao.TeacherAnnoDao.findById,fetchTypeFetchType.LAZY// 延迟加载))})ListStudentfindAllLazy();}// TeacherAnnoDao接口publicinterfaceTeacherAnnoDao{Select(select * from teacher where id #{id})TeacherfindById(Integerid);}一对多查询老师→学生publicinterfaceTeacherAnnoDao{Select(select * from teacher)Results({Result(propertyid,columnid),Result(propertyTname,columnTname),// 一对多关联Many注解Result(propertystudents,columnid,manyMany(selectcom.qcby.dao.StudentAnnoDao.findByTeacherId,fetchTypeFetchType.LAZY// 延迟加载))})ListTeacherfindAllWithStudents();}// StudentAnnoDao接口publicinterfaceStudentAnnoDao{Select(select * from student where t_id #{t_id})ListStudentfindByTeacherId(Integert_id);}4. 注解开发注意事项结果集映射复用使用Results(idxxx)定义结果集通过ResultMap(xxx)复用避免重复代码。动态SQL注解方式支持简单的动态SQL如script标签但复杂动态SQL建议使用XML。Select(scriptselect * from user where if testusername ! nulland username like concat(%, #{username}, %)/if if testsex ! nulland sex #{sex}/if/where/script)ListUserfindByCondition(Useruser);关联查询性能注解方式的关联查询同样支持延迟加载建议使用分步查询延迟加载提升性能。事务管理增删改操作需要手动提交事务或使用factory.openSession(true)开启自动提交。5. XML vs 注解开发如何选择简单CRUD操作优先使用注解开发代码简洁高效复杂动态SQL优先使用XML可读性和可维护性更好复杂关联查询优先使用XML配置更灵活快速原型开发使用注解开发提升开发速度大型企业级项目建议使用XML便于统一管理和维护三、总结缓存机制核心要点MyBatis默认开启一级缓存SqlSession级二级缓存需要手动开启SqlSessionFactory级一级缓存存储对象引用二级缓存存储数据返回对象拷贝增删改操作会清空缓存避免脏数据分布式环境下建议使用Redis等分布式缓存替代MyBatis二级缓存注解开发核心要点注解开发简化了简单CRUD的代码编写但复杂场景不如XML灵活核心注解包括Select、Insert、Results、One、Many等关联查询同样支持立即加载和延迟加载实际开发中可以根据场景灵活选择XML或注解方式也可以混合使用MyBatis的缓存机制和注解开发是提升开发效率和系统性能的重要手段。掌握这些知识点能够让我们在实际开发中更加得心应手写出高效、可维护的代码。