十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot整合Redis与MyBatis完整项目实战:配置序列化与缓存一致性

SpringBoot整合Redis与MyBatis完整项目实战:配置序列化与缓存一致性 简介一套整合了Spring Boot、Redis与MyBatis的完整项目源码特别适合正在学习Java后端集成开发的初学者也适合需要快速搭建缓存与持久层示例的程序员。下载后可直接导入常用开发环境运行通过真实代码理解三个框架的协作方式避免从零摸索的弯路。整个压缩包共包含27个文件其中9个Java源文件与对应的编译类文件构成主体另有3个Mapper映射文件、2个配置文件、Maven包装脚本及使用说明文档资源包整体只有26KB非常轻量便于快速下载和查阅。项目采用标准Maven工程布局源码目录区分主代码与测试代码依赖关系在配置文件中清晰声明Redis连接和MyBatis数据源均已设置完毕业务代码中包含RedisTemplate的典型操作方法、缓存注解的实用示例以及Mapper接口与XML映射的实现细节从配置到调用流程完整便于对照学习。目前已有1486人学习下载这份源码既可作为课程设计或毕业设计的参考工程也能在日常开发中充当集成模板帮助开发者节省环境搭建与排错时间快速完成功能模块开发。1. SpringBoot 整合 Redis 与 MyBatis 的完整项目到底在解决什么问题搜“SpringBoot 整合 Redis、mybatis 完整项目源码下载”的开发者大多不是没见过这三个技术名词而是被“版本匹配”和“配置写法”卡住了依赖加了一堆启动直接报Failed to configure a DataSource缓存存进去是乱码取出来类型转换异常Mapper 接口和 XML 文件的位置没对齐运行期才抛Invalid bound statement。所谓“完整项目”本质是一个能同时跑通数据库读写和缓存读写的基线工程它把 Spring Boot 的自动配置、MyBatis 的 SqlSessionFactory 装配、Redis 的连接池与序列化这三条链路一次性打通。这篇博文按这个基线拆开讲新手可以照着拼出可运行项目熟手也能看到序列化选型、缓存一致性和连接池参数这些容易被默认配置掩盖的细节。完整源码下载只是起点会改、会排查才是这个标题背后真正的需求。2. 搭出可复现的 SpringBoot Redis MyBatis 项目骨架2.1 用一个最小 Maven 模块收敛依赖版本Spring Boot 整合 Redis 或 MyBatis 本身不难难点在版本组合。Spring Boot 2.x 默认用 Lettuce 作为 Redis 客户端Spring Boot 3.x 底层是 Jakarta EEjavax.*包要整体换成jakarta.*这会影响 MyBatis 官方 starter 的选择。为了避免这种连锁反应我一般建议团队直接锁一个 Spring Boot 版本然后让 MyBatis 官方 starter 和 Spring Data Redis 都跟着 Boot 的 BOM 走。pom.xml里的核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency /dependencies逻辑说明Spring Boot 2.7.18 是 2.x 最后一个稳定版本社区资料最多遇到问题最容易搜到答案MyBatis starter 2.3.2 与它兼容性较好。commons-pool2是 Lettuce 连接池的必需依赖不引入也能启动但一旦配置了lettuce.pool.max-active就会报类找不到。MySQL 驱动这里用了mysql-connector-j是 8.x 时代的新坐标老项目的mysql-connector-java依然可用二选一即可。2.2 application.yml 中 Redis 与 MyBatis 的联动配置配置文件的写法直接影响启动成败。Redis 部分要关心的不止是 host 和 port还有连接池、超时、序列化相关的前置开关MyBatis 部分则要明确 Mapper XML 的位置和实体类别名。spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 password: database: 0 timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 0 max-wait: 3000ms mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.demo.mapper: debug逻辑说明lettuce.pool.max-active控制最大连接数max-wait是获取连接的最大等待时间这两个参数在并发高的接口里是性能瓶颈点调太小会出现RedisConnectionFailureException。map-underscore-to-camel-case开启后user_name可以自动映射到userName省掉大量resultMap。log-impl配合logging.level.mapper: debug会在控制台打印 MyBatis 的执行 SQL这是排查“缓存了错误数据”的第一现场。2.3 启动类与 Mapper 扫描的三种写法工程建好后启动类默认只会扫描自身所在包及子包Mapper 接口如果放在别的包底下注册不进 Spring 容器运行期会报Invalid bound statement (not found)。常用的解法有三种。SpringBootApplication MapperScan(com.example.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }逻辑说明MapperScan是 MyBatis starter 提供的扫描注解直接写在启动类上最省事。第二种是在每个 Mapper 接口上加Mapper接口数量少可以选择第三种是同时使用MapperScan和 XML 中的namespace要求namespace必须指向接口的全限定名否则 XML 不会被绑定。很多“完整项目源码下载”后跑不起来八成的报错都集中在这个环节。3. Redis 序列化与数据类型配置不好就是乱码和异常3.1 默认序列化方式的三个隐藏问题Spring Boot 的RedisAutoConfiguration默认注入的RedisTemplateObject, Object使用JdkSerializationRedisSerializer而StringRedisTemplate使用StringRedisSerializer。直接用默认的RedisTemplate往 Redis 里写一个对象Redis Desktop Manager 里看到的是\xAC\xED\x00\x05t...这样的二进制乱码。这个乱码不影响 Java 端自己读写但跨语言读取、排查数据、以及在 Redis CLI 里做手工清理都会很别扭。相比乱码类型转换异常更致命。用RedisTemplate写入一个对象再换StringRedisTemplate去读会直接抛ClassCastException。这两个 Template 使用不同的序列化器同样的 key 存进去的字节序列不一样。实际项目里我见过最隐蔽的问题是“缓存穿透排查困难”在 Redis 里存了null但因为是 JDK 序列化CLI 里看不出这个 key 是空值还是字符串null。3.2 用 Jackson 序列化器覆盖默认配置解决思路是自定义一个RedisTemplatekey 用StringRedisSerializervalue 用GenericJackson2JsonRedisSerializer这样存进去的是可读 JSON。配置类写法如下。Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }逻辑说明key 使用字符串序列化是为了保证 key 在 Redis Desktop Manager 和命令行里可读、可筛选比如user:info:1001这种业务 key 一眼能认出来。value 使用GenericJackson2JsonRedisSerializer它会在 JSON 里写入class类型信息反序列化时能还原成原对象类型。代价是 JSON 体积比 JDK 序列化略大且如果对象的字段有删除或改名老缓存反序列化会失败所以缓存对象最好设计成独立 DTO不要直接缓存 JPA 实体。afterPropertiesSet()这一步不能省否则序列化器设置不生效。3.3 StringRedisTemplate 与 RedisTemplate 的适用边界项目里最好明确一个约定缓存操作统一走自定义的RedisTemplate计数器、分布式锁、简单字符串用StringRedisTemplate。原因是两者序列化策略不同混用会出现“同一个 key 写入两次”的错觉。Service public class UserCacheService { private final StringRedisTemplate stringRedisTemplate; private final RedisTemplateString, Object redisTemplate; public UserCacheService(StringRedisTemplate stringRedisTemplate, RedisTemplateString, Object redisTemplate) { this.stringRedisTemplate stringRedisTemplate; this.redisTemplate redisTemplate; } public void setCount(String key, long value) { stringRedisTemplate.opsForValue().set(key, String.valueOf(value)); } public void setUser(String key, UserInfo user) { redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES); } }逻辑说明setCount走 StringRedisTemplate存的是纯字符串适合接口计数、验证码这类场景setUser走 JSON 序列化适合对象缓存。两者各管一类 key用前缀区分例如count:和user:可以避免序列化器交叉混用带来的困惑。Redis 数据类型方面除了 String实际项目里 List 适合做简单的消息队列或最近浏览记录Hash 适合存对象字段频繁更新的场景Set 可以做去重和标签ZSet 主要用于排行榜。Spring Data Redis 的opsForList()、opsForHash()等 API 都对应这些数据类型面试和开发里都值得单独展开。4. MyBatis 与 Redis 的缓存协作Cacheable 与缓存一致性4.1 缓存注解加在 Service 层而不是 Mapper 层很多从“完整项目源码下载”拿到代码的人会直接把Cacheable加在 Mapper 接口的方法上。这样做在 Spring Boot MyBatis 组合里效果很差原因有两点Mapper 是接口代理注解的拦截器未必能正确识别MyBatis 自身有一级缓存和二级缓存机制和 Spring Cache 叠加会造成字段更新后缓存不刷新的问题排查起来非常费劲。常见做法是把缓存注解加在 Service 实现类的方法上这样注解的语义与“业务方法的结果可以被缓存”保持一致也比较符合事务边界的直觉。Service public class UserServiceImpl implements UserService { private final UserMapper userMapper; public UserServiceImpl(UserMapper userMapper) { this.userMapper userMapper; } Override Cacheable(value userCache, key #id) public UserInfo getUserById(Long id) { return userMapper.selectById(id); } Override CachePut(value userCache, key #user.id) public UserInfo updateUser(UserInfo user) { userMapper.updateById(user); return user; } Override CacheEvict(value userCache, key #id) public void deleteUser(Long id) { userMapper.deleteById(id); } }逻辑说明Cacheable先查缓存缓存未命中才执行方法体并写入CachePut每次都会执行方法并把返回值写回缓存适合“更新后缓存要立刻反映新值”的场景CacheEvict在方法执行后删除缓存适合删除操作。value是缓存分区名逻辑上的命名空间比如userCachekey用 SpEL 表达式从参数中取值#id对应方法入参。注意这里的缓存并不是 Redis 独有的机制而是 Spring Cache 抽象。要让它走 Redis需要额外一个CacheManager的 Bean常见做法是用RedisCacheManager取代默认的ConcurrentMapCacheManager。4.2 缓存一致性最稳妥的“双删”策略上述注解方式在单机场景下够用但在分布式场景下最简单可靠的做法不是更新缓存而是删除缓存。先更新数据库再删除缓存下次读取时重新加载这种“Cache Aside”模式是实际项目里最常用的缓存策略。真正难处理的是先删缓存、更新过程中有其他请求读到旧数据并回填的情况这时需要用“延迟双删”。Transactional public void updateUserWithCache(String userId, UserInfo newInfo) { userMapper.updateById(newInfo); stringRedisTemplate.delete(user:info: userId); // 延迟双删等待可能发生的缓存回填完成 ThreadPoolTaskScheduler scheduler new ThreadPoolTaskScheduler(); scheduler.initialize(); scheduler.schedule(() - stringRedisTemplate.delete(user:info: userId), new Date(System.currentTimeMillis() 500)); }逻辑说明第一次删除清掉旧缓存500 毫秒后再删一次是为了处理“删除瞬间有请求查询到旧值并把旧值写回缓存”的竞态窗口。延迟时间要根据业务耗时调整数据库操作超过 500 毫秒的场景要加大到 1 到 2 秒。这个策略不追求绝对的强一致但能把不一致窗口压缩到毫秒级。如果项目对一致性要求极高可以引入 Canal 监听 binlog在数据库变更后主动失效缓存那是更重的架构方案不在这里展开。4.3 MyBatis 二级缓存和 Redis 的边界划分MyBatis 自带的二级缓存默认是进程级别的本地缓存作用域是按 Mapper 划分的多个应用实例之间不会共享。把它和 Redis 同时打开会出现一种奇怪的现象同一个接口在 A 机器上读到了本地旧数据在 B 机器上读到了 Redis 新数据用户请求打到不同机器结果不一致排查的时间成本极高。缓存类型作用域共享方式适用场景MyBatis 一级缓存SqlSession单次会话内默认开启无需修改MyBatis 二级缓存Mapper namespace单机 JVM 内单实例部署、读多写少Spring Cache RedisService 方法多实例共享分布式部署、通用缓存参数说明MyBatis 一级缓存是默认开启的同一个 SqlSession 内两次相同的查询不会重复查库但 Spring 管理的 Service 方法通常每次调用都会新建 SqlSession所以一级缓存在 Spring Boot 项目里作用有限。二级缓存需要显式在 Mapper XML 里配cache/标签我建议在整合 Redis 的项目里直接关闭它所有跨进程缓存都交给 Redis 统一管理数据一致性和排查路径都清晰得多。5. Redis 分布式锁与连接池调优验证整合链路完整性5.1 用 setIfAbsent 实现最简可靠分布式锁有了 Redis 和 MyBatis很多场景需要压榨它们的协作能力比如防止多实例重复执行定时任务。最简分布式锁用setIfAbsent加过期时间即可实现但要注意 setNX 和 expire 必须原子执行否则锁过期时间设置失败进程崩溃后锁永远不释放。public class RedisLock { private final StringRedisTemplate stringRedisTemplate; private final String lockKey; private final String lockValue; private final long expireTime; public boolean tryLock() { Boolean result stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS); return Boolean.TRUE.equals(result); } public void unlock() { String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue); } }逻辑说明setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS)是原子的等价于 Redis 的SET key value NX EX。解锁时不能简单地delete要先用 Lua 脚本比较lockValue防止把别人持有的锁误删——A 的锁过期后B 获取了同 key 的锁A 这时如果直接 delete 会把 B 的锁删掉。lockValue一般用UUID 线程标识拼接保证每次加锁的值唯一。5.2 连接池参数与监控命令Redis 连接池的默认参数在压测时往往不够。max-active设太小高并发下会出现获取连接等待超时max-idle设太大闲置连接占据内存。推荐先按接口 QPS 估算单个连接可以支撑上千次简单读写所以 16 到 32 个活跃连接对大多数中小项目足够。线上验证连接池有没有耗尽可以用 Redis 自带的INFO clients命令查看connected_clients配合redis-cli --stat观察实时连接数变化。如果max-active超过 32 后 QPS 没有明显提升说明瓶颈不在 Redis 连接而在MyBatis的数据库连接池需要回过去看spring.datasource.hikari.maximum-pool-size。5.3 用缓存命中率验证整合的最终效果代码全部跑通后验证整合是否真正生效不要只看接口返回 200。在 MySQL 执行FLUSH STATUS然后连续调用两次查询用户详情的接口对比第二次调用时Handler_read_second和响应时间的变化或者在 Redis CLI 里执行MONITOR观察第二次调用时是否还有GET user:info:1001之后跟着 Mapper SQL 的请求发出。如果第二次请求没有打到数据库说明 Redis 缓存确实生效了。这个验证方法比看日志直观也适合写进项目 README 作为验收标准。5.4 整合完成后的清理动作最后一步建议关闭 MyBatis 的StdOutImpl日志输出或者把log-impl改为org.apache.ibatis.logging.slf4j.Slf4jImpl避免生产环境控制台被 SQL 刷屏。spring.redis.timeout不要设置成 0否则 Redis 挂掉时请求会无限期等待最终拖垮整个服务。把这些收尾细节做完这套整合才算真正达到可交付的状态。本文还有配套的精品资源点击获取
返回列表