拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MyBatis二级缓存核心机制与高并发查询性能优化实践

1. 二级缓存到底是什么值得配置吗先把这个概念说清楚。MyBatis的一级缓存是SqlSession级别的简单说就是在同一个SqlSession内部同样的查询语句走一遍数据库之后结果会暂存在内存里下次再执行相同的查询就直接从内存取不再发SQL到数据库。但这个缓存的生命周期太短了SqlSession一关缓存就没了。在Spring整合的场景下每次操作基本都会新建一个SqlSession用完之后立刻关闭所以一级缓存的实际命中率低得可怜多数时候等于摆设。二级缓存就不一样了它的作用域是Mapper级别的也就是同一个namespace命名空间下的所有查询语句共享一份缓存数据。缓存的生命周期跟随SqlSessionFactory只要应用不重启缓存就一直在。这意味着不同的SqlSession、不同的线程、甚至不同的请求只要查的是同一个Mapper里相同的语句都能命中这份缓存。说得直白点一级缓存是线程内的临时草稿纸二级缓存是全公司共享的资料库这才是真正能扛住高并发重复查询压力的东西。那它解决了什么问题最核心的就是减少数据库的重复查询压力。很多业务场景里热点数据其实没那么多比如配置表、字典表、分类信息、用户基础信息这些数据的查询频率极高但更新频率极低。没有二级缓存的时候每个请求进来都要实实在在打一次数据库数据库的连接池、SQL解析、索引查找、结果集映射整个链路全部跑一遍。有了二级缓存之后同样的查询第一次走数据库后续所有请求直接从内存拿结果数据库的QPS压力瞬间降下来。在IO密集型、读多写少的系统里这个收益非常明显实测中热点查询的响应时间能从三五毫秒降到微秒级。那哪些人需要重点关注这个配置如果你在用Spring Boot MyBatis做常规业务系统接口的查询逻辑里又有大量重复性查询或者你在做报表类、后台管理类应用频繁查一些不常变的基础数据那二级缓存的配置值得仔细研究。当然如果你的系统已经上了Redis做缓存二级缓存的位置会比较尴尬这个后面我会专门聊。整体来说这是一项投入成本极低但收益上限很高的优化手段配置过程也就十来分钟值得每一个MyBatis使用者掌握。2. 二级缓存的工作原理与核心机制2.1 一条查询语句在二级缓存中的完整旅程想真正用好二级缓存不能只停留在配几个参数能跑的层面得把它的执行机制摸透。一条查询语句带着参数来到MyBatis之后会经过一个严格的判断链。首先MyBatis会根据当前Mapper的namespace加上SQL语句的id再加上参数对象、分页参数等条件一起计算出一个缓存Key。这个Key的生成规则非常严格只要SQL语句文本不同、参数值不同、甚至参数对象的toString()结果不同Key就不一样。所以网上有些文章说二级缓存只要配置了就能命中这是不对的实际命中条件相当苛刻。Key计算出来之后MyBatis会拿着这个Key去查二级缓存。如果命中了直接走结果集映射把缓存里存储的对象转成返回类型返回给调用方。注意这里有个关键点MyBatis的二级缓存和Redis缓存不一样它缓存的不一定是Java对象本身而是一堆序列化之后的数据。如果配置了readOnlytrue那缓存里存的就是对象引用直接返回如果readOnlyfalseMyBatis会对缓存的数据做反序列化得到一个全新的对象副本返回。这一步是很多人踩坑的地方后面我会专门讲。如果没有命中SQL会继续执行查询数据库拿到结果集之后会先把结果放到二级缓存里然后再返回给调用方。与此同时这个数据也会进入当前SqlSession的一级缓存。整个链路就是二级缓存未命中去一级缓存碰运气一级缓存也没有才真正执行SQL查数据库。这里还有一个容易被忽略的细节MyBatis的二级缓存写入时机。它不是在查询结束立刻写入的而是要等SqlSession提交commit之后才真正把数据刷入二级缓存。你执行完查询之后如果没有commit或者没有走Spring的事务管理缓存数据其实还停留在待提交阶段并不会被其他线程看到。Spring环境下因为有事务管理器的介入查询结束后事务提交数据才会真正进入二级缓存这也是很多人在非Spring环境下测试二级缓存不生效的一个核心原因。2.2 一级缓存与二级缓存的协同关系一级缓存和二级缓存之间的协同很多人理解是反的。我要强调一个顺序问题MyBatis的查询顺序是二级缓存先行一级缓存在后。也就是说执行一条查询语句时先去看二级缓存有没有没有再去看一级缓存最后才查数据库。这个顺序决定了二级缓存的优先级更高、作用范围更广。既然二级缓存优先级更高一级缓存还有什么存在的意义它的主要作用是在同一个SqlSession内部避免完全重复的SQL在事务内部被反复执行。比如你在一个事务里先查了一次用户信息后续代码里又查了一次相同的用户中间没有更新操作那第二次查询会直接从一级缓存拿结果。虽然在实际的Spring MVC请求里一个请求往往对应一个新的SqlSession这个场景用得不多但在批量处理、定时任务这类长生命周期SqlSession的场景里一级缓存还是能省不少事的。在配置二级缓存时有一个大前提需要先搞清楚settings里的cacheEnabled全局开关必须为true二级缓存才会生效。这个参数默认值就是true所以很多人没注意过它。但如果你在某个项目里发现二级缓存怎么配都不生效第一件事就应该去检查这个开关是不是被谁改成false了。2.3 二级缓存的生命周期管理二级缓存的生命周期是跟着SqlSessionFactory走的。这就意味着只要应用进程活着缓存数据就一直在。对于那种更新极少的查询这是好事但对于频繁更新的数据表如果清理策略配置不当很容易出现数据不一致的问题。缓存的生命周期里涉及两个关键操作写入和清空。写入的时机前面说了是在事务提交之后清空的时机则跟增删改语句有关。MyBatis有个设计原则在同一个namespace下任何一条insert、update、delete语句执行时都会清空整个二级缓存区域而不是只清空受影响的那条数据。这个无差别清空策略很粗犷但它是为了最大程度保证数据一致性。因为你很难精确判断一条更新语句到底影响了哪些缓存Key索性全部清掉下次查询重新从数据库加载。这个清理机制还会带来一个连锁效应如果你在一个事务里先执行了一次查询又执行了一次更新操作那这个查询的缓存结果就不会被写入二级缓存了因为在事务提交时MyBatis会检测到当前namespace有更新操作发生直接把待写入的缓存丢弃。这是很多人在同一个Mapper里既查又改之后发现二级缓存命中率急剧下降的原因。3. 二级缓存的具体配置步骤与参数详解3.1 全局配置开启总开关与确认环境这一步虽然简单但很多人真的忽略。在MyBatis的全局配置文件mybatis-config.xml中需要确保settings配置里有这样一段settings setting namecacheEnabled valuetrue/ /settings如果你用的是Spring Boot集成通常在application.yml或application.properties里配置形式是这样的mybatis: configuration: cache-enabled: true这里说一下为什么默认值本身就是true我还要专门提它。因为在很多Spring Boot项目中开发者会自定义MyBatis的Configuration类或者引入一些第三方组件不经意间把这个开关改掉。而且这个开关是二级缓存的总闸总闸没开后面一切配置都是白搭。除了这个总开关还要确保实体类实现了Serializable接口。因为MyBatis二级缓存在默认情况下会序列化缓存对象如果你的实体类没有实现Serializable程序运行到缓存写入或读取时直接抛java.io.NotSerializableException。这不是配个参数能绕过去的属于硬性要求。3.2 Mapper中开启二级缓存的三种方式二级缓存的配置核心在Mapper层。最传统的方式是在Mapper的XML文件中添加cache/标签mapper namespacecom.example.mapper.UserMapper cache/ select idselectById resultTypecom.example.entity.User select * from user where id #{id} /select /mapper这是最小配置就一个空标签MyBatis会用默认的PerpetualCache作为缓存实现使用LRULeast Recently Used最近最少使用淘汰策略。这种方式适合快速验证效果但不是生产环境的最优解。第二种方式是对cache/标签进行详细配置比如cache evictionLRU flushInterval60000 size512 readOnlytrue blockingtrue/第三种方式是使用注解开启。如果你的项目是纯注解方式写SQL的可以在Mapper接口上加CacheNamespace注解CacheNamespace(eviction LruCache.class, flushInterval 60000, size 512, readWrite true) public interface UserMapper { Select(select * from user where id #{id}) User selectById(Long id); }这里需要说明的是如果XML和注解混用且XML里已经配了cache/注解里的CacheNamespace会冲突启动时会报错。建议一个Mapper的缓存配置方式保持统一。3.3 关键参数逐个拆解先说eviction这是缓存淘汰策略。默认是LRU也就是当缓存空间不够时优先淘汰最久没有被访问的数据。这个策略在绝大多数业务场景里表现都很好因为热点数据会被频繁访问自然不容易被淘汰。除了LRU还有FIFO先进先出、SOFT软引用、WEAK弱引用三种。FIFO按进入缓存的先后顺序淘汰对访问频率不敏感适合数据访问比较均匀的场景。SOFT和WEAK依赖JVM的垃圾回收机制当内存紧张时会被GC回收适合对缓存可靠性要求不高、但内存资源比较紧张的系统。我个人在实际项目中基本只用LRU其他策略在业务系统里很难找到非用不可的理由。flushInterval是缓存刷新间隔单位是毫秒。默认值是0表示没有固定的刷新时间缓存的清理完全依赖增删改操作触发。设了具体值之后比如60000就表示每过60秒缓存自动清空一次。这个参数对数据变更不频繁但变更时机不确定的业务很有用可以给缓存设置一个最大有效期防止数据长期不刷新。size是缓存最多能存储的对象数量默认值是1024。注意这里说的是对象数量不是内存大小。配置多大合适没有标准答案要看你的业务数据量和单个对象的大小。但有一个坑需要提醒size设得过大如果访问模式又比较分散会导致缓存中存了大量不常用的数据反而挤压了热点数据的空间。我见过有人把这个值配成100000结果缓存命中率不升反降。更稳妥的做法是先用默认值观察命中率再根据实际情况调整。readOnly这个参数非常关键。默认值是false表示读写模式read-writeMyBatis会返回缓存对象的序列化副本调用方对返回结果做任何修改都不会影响缓存里的原始数据当它为true时MyBatis直接返回缓存对象的引用调用方修改了返回对象缓存里的数据也会跟着变。从隔离和安全性角度来说readOnlyfalse更安全但序列化和反序列化会带来额外的CPU开销。如果查询返回的数据量很大这个开销会比较明显。如果确定调用方不会修改返回对象配置readOnlytrue能获得更好的性能。blocking参数默认是false。当它设为true时如果某个缓存Key未命中MyBatis会阻塞后续相同Key的查询请求只让第一个请求去查数据库并构建缓存后面的请求等待缓存构建完成后直接读取。这对防止缓存击穿非常有效但也要注意如果那个唯一的查询执行得很慢所有等待的线程都会跟着阻塞。使用这个参数前最好评估一下你的查询耗时。3.4 细粒度控制useCache与flushCachecache/标签配好之后是针对整个Mapper生效的。但实际业务中同一个Mapper里不同的查询语句对缓存的需求可能不一样。比如UserMapper里有个selectById方法数据不常变适合走缓存还有一个selectByStatus方法数据变化频繁不适合走缓存。这种场景就需要在语句级别做细粒度控制。在select语句上添加useCachefalse可以指定这条查询不使用二级缓存select idselectByStatus resultTypecom.example.entity.User useCachefalse select * from user where status #{status} /select反过来对于insert、update、delete语句可以添加flushCache属性。flushCachetrue表示执行该语句时清空二级缓存这是默认行为flushCachefalse则表示执行该语句后不清空缓存。这个属性在实际项目中用得不多因为默认的清空行为更安全。但也有特殊情况比如你明确知道某条更新语句修改的数据不影响缓存中的任何内容例如更新一个表中从未被缓存过的字段设置flushCachefalse可以提高缓存命中率。这种做法有风险需要业务上非常确认才建议使用。另外还有一个容易忽略的点select语句的flushCache属性其实默认是false如果把它设为true那么这条查询每次执行前都会清空二级缓存同时这条查询本身的结果也不会被缓存。这相当于完全绕过了缓存机制适合那种必须实时查数据库的少数查询。3.5 集成第三方缓存EhCache与RedisMyBatis自带的PerpetualCache是基于HashMap的本地缓存功能简单只适合单机应用和快速验证。生产环境通常要接外部缓存组件。这里着重说两种最常见的EhCache和Redis。先看EhCache的配置方式。首先引入依赖dependency groupIdorg.mybatis.caches/groupId artifactIdmybatis-ehcache/artifactId version1.2.3/version /dependency然后在Mapper的XML中把cache/替换为cache typeorg.mybatis.caches.ehcache.EhcacheCache property nameeternal valuefalse/ property nametimeToIdleSeconds value3600/ property nametimeToLiveSeconds value3600/ property namemaxEntriesLocalHeap value1000/ property namememoryStoreEvictionPolicy valueLRU/ /cacheEhCache的优势是纯本地缓存基于磁盘和堆外内存的扩展能力很强缓存数据量可以做得很大而且访问速度比Redis快得多毕竟少了一次网络IO。再看Redis集成。引入依赖dependency groupIdorg.mybatis.caches/groupId artifactIdmybatis-redis/artifactId version1.0.0-beta2/version /dependency对应的缓存配置是cache typeorg.mybatis.caches.redis.RedisCache property namehost value127.0.0.1/ property nameport value6379/ property namepassword value/ property nametimeout value5000/ property namedatabase value0/ /cache当然生产环境中一般不会直接在Mapper配置里写死Redis连接信息而是通过自定义RedisCache类从Spring容器或配置中心拉取Redis连接。Redis缓存的好处显而易见缓存数据集中存储多实例部署时各个节点共享同一份缓存不存在因本地缓存各自为政导致的数据不一致问题。这里我要给一个阶段性结论如果你的系统已经全面使用了Redis做业务缓存MyBatis的二级缓存其实没有太大必要再接Redis了。因为Service层自己用Redis管理热点数据远比让MyBatis内部去管理更灵活、更可控。二级缓存的价值更多体现在本地进程级缓存这一层也就是单机部署、查询密集、数据低频更新的场景。4. 缓存失效机制与常见坑点4.1 数据一致性这个坑最容易爆二级缓存最大的风险就是数据不一致。前面提到过MyBatis的缓存清空策略是只要namespace下有增删改就清空整个namespace的缓存。这个策略在单表操作时比较安全但一旦涉及多表关联查询问题就来了。举个例子。假设你有OrderMapper和OrderItemMapper两个Mapper。OrderMapper里有一个查询订单及其明细的SQL这个SQL join了order和order_item两张表缓存自然放在了OrderMapper的namespace下。现在用户新增了一条订单明细走的是OrderItemMapper的insert语句它只会清空OrderItemMapper自己的缓存而OrderMapper里那份包含旧明细数据的缓存依然保留着。如果后续有人查询同一个订单就会拿到旧的明细数据而且这份数据要等到OrderMapper有增删改、或者缓存过期才会被刷新。这个问题在业界有一个标准的解决方案把关联查询放到同一个Mapper里或者引入缓存引用区域来控制缓存作用域。MyBatis提供了cache-ref标签可以将当前Mapper的缓存指向另一个namespace的缓存。比如mapper namespacecom.example.mapper.OrderItemMapper cache-ref namespacecom.example.mapper.OrderMapper/ /mapper这样配置之后OrderItemMapper的增删改操作会清空OrderMapper的缓存区域从而保证关联数据的一致性。但这样做的代价是缓存作用域变大OrderMapper里任何一条增删改语句都会导致OrderItemMapper的数据也一起被清空。实践中需要权衡。还有一个小技巧是在实体设计上规避。对于多表关联查询的复杂结果不要走MyBatis二级缓存直接走Service层的Redis缓存由业务代码自己控制失效粒度。MyBatis二级缓存更适合单表简单查询这个定位心里要有数。4.2 序列化与readOnly的取舍很多团队第一次配二级缓存跑起来头几天一切正常突然某一天线上报了一个NotSerializableException排查半天发现是新增的一个查询返回值里嵌套了一个没实现Serializable的对象。这个问题的根本原因在于默认的readOnlyfalse模式下缓存数据的写入和读取都要做序列化和反序列化。序列化带来的问题不止是代码规范上的。它对性能有实际影响每次缓存命中都要把存储的字节数组反序列化成Java对象如果你的查询结果是一个很大的列表这个反序列化过程可能要消耗几毫秒甚至更多直接把缓存的性能优势吃掉一大截。这个问题怎么处理如果你非常确定被缓存的数据不会被外部修改直接设置readOnlytrue。这个模式下MyBatis不会做序列化缓存里存的就是对象本身命中后直接返回引用速度快得多。代价是如果有人对返回结果做了修改缓存数据会被污染下次其他请求拿到的是被改过的数据。所以这个参数要在性能和安全之间做取舍没有绝对正确的选择。我的建议是对于字典数据、配置数据这类只读场景放心大胆用readOnlytrue对于业务数据还是老老实实用默认的读写模式。4.3 事务隔离级别与脏读问题上次在项目里排查一个诡异的问题一个接口查出来的数据总是旧的明明数据库里已经更新了接口查出来的却还是上一个值。后来发现问题出在一个长事务上。事务A开启后先查了一次用户信息此时数据被标记为待写入缓存但还没真正进入二级缓存因为事务还没提交。事务B此时更新了这条用户数据并提交。事务A后续又查了一次相同的用户信息一级缓存里还有旧值所以返回的是旧的等到事务A提交的时候MyBatis把第一次查询的旧结果写入了二级缓存并且因为事务A是查询为主这个写入动作把事务B更新后的正确数据给覆盖了后续所有请求都拿到了旧数据。这个案例说明二级缓存的写入时机和事务提交强绑定长事务场景下缓存数据的时效性很难得到保证。规避思路有两个一是在代码层面避免长事务尽量在业务操作完成后及时提交事务二是在容易产生数据不一致的Mapper上关闭二级缓存或者设置较短的flushInterval让脏数据尽快过期。4.4 多实例部署时的缓存共享问题如果你的应用是单机部署二级缓存用默认的PerpetualCache完全没问题。但一旦做了多实例部署比如两台服务器跑同一个应用前面挂负载均衡问题立刻出现每个实例的二级缓存是彼此独立的实例A缓存了一份数据实例B可能缓存了另一份两边数据不一致。这个问题没有银弹。方案一改用RedisCache让所有实例共享同一个Redis里的缓存数据。方案二关掉MyBatis二级缓存在Service层通过Redis统一管理热点数据。方案三如果你的业务场景能接受短时间的不一致保留本地缓存并设置较短的flushInterval。从实际经验来看方案二才是大多数微服务架构下的正确选择因为MyBatis二级缓存的强项是单机本地缓存它的设计初衷就没打算解决分布式缓存的问题。5. 常见问题排查与性能调优实录5.1 快速定位二级缓存命中率怎么统计配置了二级缓存之后怎么知道它到底有没有起作用不能靠感觉得看数据。MyBatis提供了一个Cache接口的统计能力在日志中开启对应的日志级别可以看到缓存命中的日志记录。如果你的项目用的是Logback或Log4j2可以针对org.apache.ibatis.cache包设置debug级别logging: level: org.apache.ibatis.cache: debug这样设置后命中缓存时日志会输出类似Cache Hit Ratio [com.example.mapper.UserMapper]: 0.85的内容。这个比例就是二级缓存的命中率。一般来说命中率超过0.6缓存配置就有实际意义低于0.3说明缓存配置有问题或者查询本身不集中需要检查useCache配置和数据访问模式。除了看日志还可以通过JMX监控SqlSessionFactory的缓存信息集成到Prometheus和Grafana里做可视化监控这个在大型系统中比较常用。5.2 排查实录配置了却不生效这里记录一个典型的排查过程。有一次同事反馈说某个Mapper的cache/标签明明加了二级缓存就是不生效。我让他把日志级别调到TRACE仔细看查询日志发现每次查询都执行了SQL缓存命中率始终为0。逐项排查全局开关是好的实体类也实现了Serializablecache/也加了看起来都没问题。最后发现问题出在Mapper接口的包扫描上。项目里用了MapperScan扫描接口同时Mapper XML文件也放在对应的包路径下但XML文件里的namespace写错了少了一段前缀。MyBatis加载这个XML时把它当成了一个独立的Mapper与接口对不上查询走的是接口绑定的另一个SQL定义所以二级缓存根本挂不上。namespace写错这个低级错误在项目里出现频率比想象中高得多。检查方法很简单MyBatis启动时会打印Mapper的加载日志看看namespace是否和你预期的完全一致。5.3 性能对比开缓存前后数据说话用一个实际案例给大家参考。有次给一个后台管理系统做优化系统里有个查询订单列表的接口表里大概有50万条数据过滤条件组合很多但某个高频查询按状态和创建时间查某天的订单每秒被调用几十次。数据库压力很大高峰期这块查询能把CPU打到80%以上。我们在这个查询对应的Mapper上配置了二级缓存设置LRU策略、size为2048、flushInterval设置为5分钟。配置上线后数据库CPU直接从80%降到40%左右查询接口的P99响应时间从120ms降到3ms。因为订单数据有新增操作新增发生时缓存会被清空所以命中率不算特别高大约在0.55左右但已经明显减轻了数据库压力。另一个案例则提醒我要慎重。有个项目的用户信息查询我当时图省事给UserMapper配了二级缓存结果用户更新资料的接口每次执行后都会清空整个用户缓存导致查询命中率极低只有0.1左右缓存不但没有减轻数据库压力反而因为序列化和缓存管理增加了CPU开销。后来果断去掉二级缓存改成在Service层用Redis针对单个用户ID做缓存效果好很多。这个对比说明一个道理二级缓存不是万能的最适合它的场景是数据更新频率低的查询一旦数据更新频率和查询频率相当缓存的意义就大打折扣。5.4 参数调优的方法论网上很多文章会直接给出一套最佳配置什么size512、flushInterval60000、evictionLRU好像所有项目都能套用。我实际做过很多次调优之后可以负责任地说没有一套配置能通吃所有场景。合理的调优路径应该是这样的先用基础配置把缓存跑起来观察日志中的命中率。如果命中率低先排查是不是缓存Key分散比如查询条件过于多样或者数据更新过于频繁导致缓存常被清空。如果命中率正常再考虑提升性能比如调整readOnly参数。如果内存压力大再调整size和eviction策略。一个值得参考的配置是读多写少、热点集中的查询场景用LRU size偏大 readOnlytrue 不设flushInterval完全依赖增删改清空读多写少但热点分散的场景用LRU size适中 readOnlytrue 设置较长的flushInterval读写均衡的场景建议考虑关掉二级缓存改用Redis。6. 实操心得与踩坑记录这篇文章写到这里核心配置已经讲完了但有一些实际工作中沉淀下来的体会我觉得比配置本身更值得分享。先说说事务和二级缓存的微妙关系。一开始我用二级缓存时天真地以为只要查询走了Mapper就会自动缓存。直到有一次调试一个定时任务跑了半个小时缓存命中率一直是0。后来才意识到那个定时任务的方法上没有加Transactional注解查询结束后SqlSession直接关闭事务没有提交缓存数据根本没有写入二级缓存。可能有人会说MyBatis在SqlSession关闭时会自动提交事务为什么还有这个问题实际上在Spring整合场景下SqlSession是SqlSessionTemplate管理的它的事务行为绑定的是Spring的事务管理器没有Spring事务界定缓存写入的时机就无法保证。所以如果你的二级缓存命中率上不去先检查一下查询方法是否在Spring事务上下文里。再说一个序列化的坑。默认的readOnlyfalse模式下如果结果集里有一个Map类型字段或者一个包含复杂类型的对象反序列化时可能因为类型信息丢失导致转换异常。这类问题排查起来很痛苦因为不是每次都必现而是取决于缓存是否命中。遇到这种问题最快的处理方式是给相关的查询语句设置useCachefalse把这个查询移出缓存体系。然后是关于cache-ref的使用经验。前面提到多表关联查询的缓存一致性问题用cache-ref可以把多个Mapper的缓存归一到同一块区域。但使用时要格外小心因为所有引用同一缓存的Mapper任何一个的执行增删改都会清空整块缓存区域。如果你的系统里订单Mapper和订单明细Mapper都引用了同一个缓存那订单明细的每次新增都会导致订单查询缓存失效反过来订单的每次状态修改也会连带把明细缓存清掉。如果两个Mapper各自的数据更新频率都高这种关联清理反而会让缓存命中率变得很低。我在一个项目中就是这样做的结果缓存命中率不到0.2后来改成了按业务场景单独管理缓存问题才解决。最后分享一个小技巧。在开发调试阶段可以按Mapper维度临时关闭二级缓存排除缓存干扰。比如在XML配置中把cache/注释掉或者把cacheEnabled设置为false只影响测试环境发布生产时再打开。对于那种开发环境正常、生产环境数据总是旧的诡异问题这个手段能帮你快速定位问题是不是缓存引起的。二级缓存这套东西配置简单真正要驾驭住需要对它的缓存策略、序列化机制、事务交互、清理逻辑都有清晰的认知。建议在实际项目中逐步引入先用统计日志摸清缓存行为模式再逐步调优不要一上来就配一堆参数追求最佳实践。缓存是性能优化手段不是业务逻辑的一部分任何一次缓存相关改动都要围绕数据一致性和性能收益这两个核心指标去衡量。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门