当前位置: 首页 > news >正文

java每日精进 5.22【多数据源(读写分离)、事务】

1.多数据源

 <!-- Dynamic Datasource for Multi-Datasource --><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>${datasource.version}</version></dependency><!-- Druid --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency>
spring:# 禁用不必要的自动配置autoconfigure:exclude:- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure  # 排除Druid数据源的自动配置,通常用于自定义配置时避免冲突datasource:druid:web-stat-filter:enabled: true  # 启用Druid的Web统计过滤器,用于监控Web应用的数据访问情况stat-view-servlet:enabled: true  # 启用Druid的监控页面Servleturl-pattern: /druid/*  # 设置监控页面的访问路径login-username: admin  # 监控页面的登录用户名login-password: 123456  # 监控页面的登录密码filter:stat:  # SQL统计过滤器配置enabled: true  # 启用SQL统计功能log-slow-sql: true  # 启用慢SQL日志记录slow-sql-millis: 100  # 定义慢SQL的标准(毫秒),执行时间超过此值的SQL会被记录merge-sql: true  # 合并统计相同的SQL(避免重复统计)wall:  # SQL防火墙配置config:multi-statement-allow: true  # 允许执行多条SQL语句(默认false,出于安全考虑)dynamic:  # 动态数据源配置primary: master  # 设置默认/主数据源的名称为"master"druid:  # Druid连接池配置initial-size: 1  # 初始化连接池中的连接数min-idle: 1  # 连接池中最小的空闲连接数max-active: 20  # 连接池中最大的活跃连接数max-wait: 600000  # 获取连接时的最大等待时间(毫秒)time-between-eviction-runs-millis: 60000  # 检测空闲连接的间隔时间(毫秒)min-evictable-idle-time-millis: 300000  # 连接在池中最小的空闲时间(毫秒),超过此时间可能被回收max-evictable-idle-time-millis: 900000  # 连接在池中最大的空闲时间(毫秒),超过此时间会被回收validation-query: SELECT 1  # 用于检测连接有效性的SQL语句test-while-idle: true  # 空闲时是否测试连接的有效性test-on-borrow: false  # 获取连接时是否测试其有效性(通常不建议开启,影响性能)test-on-return: false  # 归还连接时是否测试其有效性(通常不建议开启,影响性能)datasource:  # 具体数据源配置master:  # 主数据源配置url: jdbc:mysql://localhost:3306/buysys?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai  # 数据库连接URLusername: root  # 数据库用户名password: 123456  # 数据库密码driver-class-name: com.mysql.cj.jdbc.Driver  # JDBC驱动类名slave:  # 从数据源配置lazy: true  # 是否懒加载(延迟初始化)url: jdbc:mysql://localhost:3306/mysql_test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai  # 从数据库连接URLusername: root  # 从数据库用户名password: 123456  # 从数据库密码driver-class-name: com.mysql.cj.jdbc.Driver  # JDBC驱动类名
@Repository
public interface StudentMapper extends BaseMapper<Student> {// 从从库(mysql_test)查询学生信息,支持分页和按名称模糊查询@DS("slave")default IPage<Student> selectStudentPage(Page<Student> page, @Param("sName") String sName) {LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();if (sName != null && !sName.isEmpty()) {queryWrapper.like(Student::getSName, sName);}return this.selectPage(page, queryWrapper);}
}

2.事务

2.1 单机 + 单数据源:@Transactional 注解

描述
  • 适用场景:单机环境中,操作单一数据源(例如只操作 buysys 数据库的 user 表)。
  • 实现方式:使用 Spring 的 @Transactional 注解,声明事务以确保数据一致性。
  • 特点
    • 简单易用,适合大多数单数据源场景。
    • Spring 自动管理事务的开启、提交和回滚。
    • 如果方法抛出异常,事务会自动回滚。
代码示例

以下是一个在 UserService 中使用 @Transactional 注解的示例,操作 buysys 数据库的 user 表。

/*** 用户服务实现类,操作单一数据源(buysys 数据库)。*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {/*** 创建用户并更新用户状态,事务确保两操作一致性。* 如果任一操作失败,整个事务回滚。*/@Transactionalpublic void createAndUpdateUser(User user, String newStatus) {// 插入用户save(user);// 更新用户状态(模拟后续操作)user.setStatus(newStatus);updateById(user);// 模拟异常,测试事务回滚if ("error".equals(newStatus)) {throw new RuntimeException("Simulated error to trigger rollback");}}
}
注释解释
  • @Transactional
    • 标记在方法上,开启事务管理。
    • 默认使用 master 数据源(buysys 数据库),由 Spring DataSource 自动配置。
    • 如果方法抛出未捕获的异常(如 RuntimeException),事务会回滚,save 和 updateById 操作都不会生效。
  • 适用性:适用于您的 UserController 中对 user 表的增删改操作(如 createUser, updateUser)。
  • 注意事项
    • 确保 spring.datasource 配置正确,指向单一数据源。
    • @Transactional 默认传播行为是 REQUIRED,即如果当前存在事务则加入,否则新建事务。

2.2 单机 + 多数据源:@DSTransactional 注解

描述
  • 适用场景:单机环境中,操作多个数据源(例如 buysys 的 user 表和 mysql_test 的 student 表)。
  • 实现方式:使用 Dynamic Datasource 提供的 @DSTransactional 注解,结合 @DS 注解切换数据源。
  • 特点
    • 支持在单一方法中操作多个数据源。
    • 提供相对可靠的事务一致性,但不绝对(例如,某个数据源提交失败可能导致不一致)。
    • 外层 @DSTransactional 控制整体事务,内层 @DS 指定数据源。
代码示例

以下是一个示例,展示 UserService 调用 StudentService 和自身方法,分别操作 buysys 和 mysql_test 数据库。

/*** 用户服务实现类,操作多数据源(buysys 和 mysql_test)。*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate StudentService studentService;/*** 创建用户(buysys.user)和学生(mysql_test.student),使用 @DSTransactional 管理多数据源事务。* 如果任一操作失败,所有数据源的操作都会回滚。*/@DSTransactionalpublic void createUserAndStudent(User user, Student student) {// 操作主数据源(buysys.user)save(user);// 操作从数据源(mysql_test.student)studentService.createStudent(student);// 模拟异常,测试事务回滚if ("error".equals(user.getUsername())) {throw new RuntimeException("Simulated error to trigger rollback");}}
}/*** 学生服务实现类,操作 mysql_test.student 表。*/
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {@DS("slave") // 指定从数据源 mysql_testpublic void createStudent(Student student) {save(student);}
}/*** 用户服务实现类,操作多数据源(buysys 和 mysql_test)。*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate StudentService studentService;/*** 创建用户(buysys.user)和学生(mysql_test.student),使用 @DSTransactional 管理多数据源事务。* 如果任一操作失败,所有数据源的操作都会回滚。*/@DSTransactionalpublic void createUserAndStudent(User user, Student student) {// 操作主数据源(buysys.user)save(user);// 操作从数据源(mysql_test.student)studentService.createStudent(student);// 模拟异常,测试事务回滚if ("error".equals(user.getUsername())) {throw new RuntimeException("Simulated error to trigger rollback");}}
}/*** 学生服务实现类,操作 mysql_test.student 表。*/
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {@DS("slave") // 指定从数据源 mysql_testpublic void createStudent(Student student) {save(student);}
}
注释解释
  • @DSTransactional
    • 标记在 createUserAndStudent 方法上,管理多个数据源的事务。
    • 确保 save(user)(buysys 数据库)和 studentService.createStudent(student)(mysql_test 数据库)在同一事务中。
    • 如果任一操作抛出异常,所有数据源的操作都会回滚。
  • @DS("slave")
    • 指定 createStudent 方法使用 slave 数据源(mysql_test 数据库)。
    • UserServiceImpl.save 默认使用 master 数据源(buysys)。
  • 事务一致性
    • 问题①解答:如果 createStudent 失败(例如抛出异常),@DSTransactional 确保 save(user) 和 save(student) 都回滚。
    • 问题②解答:在 createStudent 方法上添加 @DSTransactional 不会影响外层事务,因为 Dynamic Datasource 会将内层事务合并到外层事务中。
  • 注意事项
    • 如果某个数据源的事务提交失败(例如网络问题),可能导致不一致。需监控日志并考虑补偿机制。
    • 确保 application.yml 中正确配置了 master 和 slave 数据源。

2.3 多机 + 单/多数据源:Seata 分布式事务

描述
  • 适用场景:分布式系统,涉及多台机器、单个或多个数据源(例如微服务架构中不同服务访问 buysys 和 mysql_test)。
  • 实现方式:使用 Seata 分布式事务框架,通过全局事务管理(TC - Transaction Coordinator)协调多个数据源或服务的事务。
  • 特点
    • 适用于微服务架构,确保跨服务的数据一致性。
    • 支持 AT(自动事务)、TCC、Saga 等事务模式。
    • 配置复杂,需部署 Seata Server。
代码示例

以下是一个简单的 Seata 分布式事务示例,模拟两个服务(UserService 和 StudentService)分别操作 buysys 和 mysql_test。

/*** 全局事务服务,协调分布式事务,涉及 buysys.user 和 mysql_test.student。*/
@Service
public class GlobalTransactionService {@Autowiredprivate UserService userService;@Autowiredprivate StudentService studentService;/*** 使用 Seata 分布式事务协调 user 和 student 表的操作。* 如果任一操作失败,所有操作回滚。** @param user 用户对象,存储到 buysys.user* @param student 学生对象,存储到 mysql_test.student*/@GlobalTransactionalpublic void createUserAndStudent(User user, Student student) {// 操作主数据源(buysys.user)userService.save(user);// 操作从数据源(mysql_test.student)studentService.createStudent(student);// 模拟异常,测试分布式事务回滚if ("error".equals(user.getUsername())) {throw new RuntimeException("Simulated error to trigger rollback");}}
}/*** 用户服务实现类*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {public void save(User user) {super.save(user);}
}/*** 学生服务实现类*/
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {@DS("slave")public void createStudent(Student student) {save(student);}
}
注释解释
  • @GlobalTransactional
    • Seata 提供的注解,标记全局事务。
    • 由 Seata 的 Transaction Coordinator(TC)协调,记录每个分支事务(userService.save 和 studentService.createStudent)。
    • 如果任一分支事务失败,Seata 会触发全局回滚。
  • @DS("slave")
    • 仍使用 Dynamic Datasource 切换到 mysql_test 数据源。
    • Seata 与 Dynamic Datasource 兼容,支持多数据源的分布式事务。
  • 事务一致性
    • Seata 的 AT 模式通过记录 SQL 操作的快照(undo log),在异常时自动回滚。
    • 相比 @DSTransactional,Seata 提供更强的一致性,适合分布式环境。
  • 注意事项
    • 需要部署 Seata Server(TC),并配置 Seata 客户端。
    • 每个服务需添加 Seata 依赖并配置事务组。

3.缓存机制

<!-- Redisson for Redis --><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>${redisson.version}</version></dependency>
/*** Redis 配置类,用于自定义 RedisTemplate 的序列化方式。* 通过此配置,RedisTemplate 支持字符串键和 JSON 格式的值,并兼容 Java 8 时间类型。*/
@Configuration
@AutoConfigureBefore(org.redisson.spring.starter.RedissonAutoConfiguration.class)
public class RedisConfig {/*** 定义 RedisTemplate Bean,用于与 Redis 交互。* 配置键使用字符串序列化,值使用 JSON 序列化,支持复杂对象存储。** @param factory RedisConnectionFactory,由 Spring Boot 自动注入,用于创建 Redis 连接* @return 自定义配置的 RedisTemplate 实例*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {// 创建 RedisTemplate 实例,指定键为 String 类型,值为 Object 类型(支持任意对象)RedisTemplate<String, Object> template = new RedisTemplate<>();// 设置 Redis 连接工厂,用于建立与 Redis 服务器的连接template.setConnectionFactory(factory);// 设置键的序列化器为 StringRedisSerializer,确保键以 UTF-8 字符串存储,便于调试template.setKeySerializer(RedisSerializer.string());// 设置哈希键的序列化器为 StringRedisSerializer,用于 Redis 哈希数据结构的键template.setHashKeySerializer(RedisSerializer.string());// 设置值的序列化器为自定义 JSON 序列化器,支持复杂对象序列化为 JSONtemplate.setValueSerializer(buildRedisSerializer());// 设置哈希值的序列化器为自定义 JSON 序列化器,用于哈希数据结构的值template.setHashValueSerializer(buildRedisSerializer());// 返回配置好的 RedisTemplate 实例,注册为 Spring Beanreturn template;}/*** 创建自定义 JSON 序列化器,基于 Jackson2JsonRedisSerializer。* 配置 ObjectMapper 以支持 Java 8 时间类型(如 LocalDateTime)的序列化和反序列化。** @return 配置好的 RedisSerializer 实例*/private RedisSerializer<?> buildRedisSerializer() {// 创建 Jackson2JsonRedisSerializer,用于将 Java 对象序列化为 JSON 字符串RedisSerializer<Object> json = RedisSerializer.json();// 通过反射获取 Jackson2JsonRedisSerializer 内部的 ObjectMapper// 注意:反射访问私有字段不是最佳实践,建议直接构造 ObjectMapper(见改进建议)ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");// 注册 JavaTimeModule,支持 Java 8 时间类型(如 LocalDate, LocalDateTime)的序列化objectMapper.registerModules(new JavaTimeModule());// 返回配置好的 JSON 序列化器return json;}
}

3.1编程式缓存

/*** 自定义分页查询用户订单(字段内嵌),使用编程式缓存。* 缓存键为 "userOrders:{status}:{username}:{page}:{size}",有效期 1 小时。*/@Override@Cacheable(value = "userOrders", key = "#status + ':' + #username + ':' + #page + ':' + #size", unless = "#result == null")public IPage<UserOrderDetail2DO> getUserOrderPage2(int page, int size, Integer status, String username) {String cacheKey = "userOrders:" + generateCacheKey(status, username, page, size);logger.debug("尝试从 Redis 获取缓存: {}", cacheKey);IPage<UserOrderDetail2DO> cachedResult = (IPage<UserOrderDetail2DO>) redisTemplate.opsForValue().get(cacheKey);if (cachedResult != null) {logger.debug("缓存命中: {}", cacheKey);return cachedResult;}logger.debug("缓存未命中,查询数据库: {}", cacheKey);Page<UserOrderDetail2DO> userOrderPage = new Page<>(page, size);IPage<UserOrderDetail2DO> result = userMapper.selectList2ByStatusAndUsername(userOrderPage, status, username);redisTemplate.opsForValue().set(cacheKey, result, 1, TimeUnit.HOURS);logger.debug("已缓存结果: {}", cacheKey);return result;}
/*** 生成用户订单缓存键。*/private String generateCacheKey(Integer status, String username, int page, int size) {return status + ":" + (username != null ? username : "") + ":" + page + ":" + size;}
    /*** 清除用户订单缓存(编程式)。*/@Overridepublic void clearUserOrderCache(Integer status, String username) {String pattern = "user:" + (status != null ? status : "*") + ":" + (username != null ? username : "*") + ":*:*";String pattern2 = "user*:*";Set<String> keys = redisTemplate.keys(pattern2);if (keys != null && !keys.isEmpty()) {redisTemplate.delete(keys);System.out.println("清除缓存: "+keys.toString());}}
    /*** 编程式缓存:保存用户,成功后清除相关用户分页缓存。*/@Override
//    @CacheEvict(value = "users", key = "'page:' + #entity.username + ':*:*'")public boolean save(User entity) {boolean result = super.save(entity);if (result) {System.out.println("用户创建成功,清除相关缓存: username="+ entity.getUsername());clearUserOrderCache(null, entity.getUsername());}return result;}

3.2声明式缓存

缓存配置类:

/*** 缓存配置类* 功能:配置 Redis 缓存管理器,解决 Java 8 日期类型序列化问题,统一缓存过期时间和序列化规则*/
@Configuration
@EnableCaching // 启用 Spring 缓存注解(@Cacheable/@CacheEvict 等)
public class CacheConfig {/*** 配置 Redis 缓存管理器* @param connectionFactory Redis 连接工厂(由 Spring Boot 自动注入)* @return RedisCacheManager 缓存管理器实例*/@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {// ------------------- 1. 配置 Jackson 序列化器(解决 Java 8 日期类型问题) -------------------// 创建 Jackson ObjectMapper(负责 Java 对象与 JSON 的转换)ObjectMapper objectMapper = new ObjectMapper();// 注册 JavaTimeModule 模块,用于支持 Java 8 日期类型(如 LocalDateTime/LocalDate)// 解决异常:com.fasterxml.jackson.databind.exc.InvalidDefinitionException 类型不支持objectMapper.registerModule(new JavaTimeModule());// 可选:自定义 JSON 格式(如日期格式为 yyyy-MM-dd HH:mm:ss)// objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));// ------------------- 2. 配置 Redis 缓存序列化规则 -------------------RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()// --------------- 缓存基本属性 ---------------.entryTtl(Duration.ofMinutes(1)) // 设置默认缓存过期时间:1 分钟.disableCachingNullValues() // 禁止缓存 null 值(避免 Redis 中存储无效键)// --------------- 键序列化规则 ---------------// 使用 StringRedisSerializer(字符串序列化器)// 优点:可读性好,便于 Redis 命令行直接查看.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))// --------------- 值序列化规则 ---------------// 使用 GenericJackson2JsonRedisSerializer(JSON 序列化器)// 传入自定义的 ObjectMapper,支持 Java 8 日期类型.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));// ------------------- 3. 构建 Redis 缓存管理器 -------------------return RedisCacheManager.builder(connectionFactory) // 基于 Redis 连接工厂构建.cacheDefaults(config) // 应用上述默认配置.build(); // 创建缓存管理器实例}
}
    /*** 自定义 XML 分页查询用户,支持声明式缓存。* 缓存键为 "users:page:{username}:{page}:{size}",缓存有效期由 spring.redis.time-to-live 控制(1 分钟)。** @param page 当前页码* @param size 每页大小* @param username 用户名(模糊查询,可为空)* @return 分页用户数据*/@Override@Cacheable(value = "users", key = "'page:' + #username + ':' + #page + ':' + #size", unless = "#result == null")public IPage<User> getUserPage(int page, int size, String username) {logger.debug("查询用户分页数据: page={}, size={}, username={}", page, size, username);Page<User> userPage = new Page<>(page, size);return userMapper.selectPageCustom(userPage, username);}/*** 更新用户,成功后清除相关用户分页缓存。*/@Override@CacheEvict(value = "users",allEntries = true)public boolean updateById(User entity) {boolean result = super.updateById(entity);if (result) {System.out.println("用户更新成功,清除相关缓存: username="+entity.getUsername());
//            clearUserOrderCache(null, entity.getUsername());}return result;}
更新用户既清理用户下所有缓存

相关文章:

  • 觉醒三境:在敦煌的风沙中寻找生命的纹路
  • 火山引擎火山云带宽价格
  • 【大模型面试每日一题】Day 26:从伦理角度,大模型可能存在哪些潜在风险?技术上如何实现内容安全控制(如RLHF、红队测试)?
  • Ubuntu-多显示器黑屏问题及nvidia显卡驱动安装
  • 当物联网“芯”闯入纳米世界:ESP32-S3驱动的原子力显微镜能走多远?
  • 自制操作系统day7(获取按键编码、FIFO缓冲区、鼠标、键盘控制器(Keyboard Controller, KBC)、PS/2协议)
  • 鸿蒙Flutter实战:23-混合开发详解-3-源码模式引入
  • FreeBSD14.2因为爆内存而导致Xfce4视窗被卡,桌面变黑色,只能看到鼠标在窗体中心,鼠标无反应,键盘无反应
  • 自制操作系统day8 (鼠标数据取得、通往32位模式之路、A20GATE、切换到保护模式、控制寄存器cr0-cr4以及cr8、ALIGNB)
  • 创建信任所有证书的HttpClient:Java 实现 HTTPS 接口调用,等效于curl -k
  • 【Java面试】从Spring Boot到Kafka:技术栈与业务场景全面剖析
  • 养生新策:五维开启健康生活
  • 青少年编程与数学 02-020 C#程序设计基础 01课题、C#编程概要
  • 现代生活健康养生新策略
  • STM32:0.96寸OLED屏驱动全解析——SSD1306 I2C通信与显存配置指南
  • 二十、面向对象底层逻辑-ServiceRegistry接口设计集成注册中心
  • AI 多 Agent 图形化开发深度解析:iVX IDE 与主流产品技术架构对比研究
  • 在 Matter.js 物理引擎中,isSensor 布尔属性的使用
  • 【AI问答】Java类中,一些变量设置了@NotNull,怎么在调用内部方法时校验变量是否为空
  • Ubuntu20.04的安装(VMware)
  • 临沂网站建设服务/企业网络规划设计方案
  • 农家院网站素材/找回今日头条
  • 杭州学校网站建设/惠州企业网站seo
  • 毕设做网站太简单/志鸿优化网官网
  • p2p网站建设报价/企业网络组建方案
  • 重庆代还信用卡网站建设/北京网站优化服务