分布式系统常用的三注解:@DS,@DistributeLock,@GlobalTransactional【开发实践】

一、多数据源的动态切换:@DS

1.1 多数据源的背景

1.1.1 主从复制

主从复制是一种数据库管理技术,通过将主服务器中的数据变更同步到从服务器上,来实现数据冗余和负载均衡。一般采用一主多从的方案,有时候也会采用多主多从的方案。主从复制的数据同步存在一定延迟。

1.1.2 读写分离

读写分离是一种基于主从复制的数据库读写策略,在应用程序层面(如在代码中或使用中间件)区分读写操作,主节点负责所有的写操作(增、删、改)和实时性较高的读操作(查),从节点负责实时性较低的读操作。在读多写少的情境下,读写分离能提升读取性能(读写分离后避免了读写锁的开销)和实现读负载均衡(使用多个从节点负责读取操作)。

1.1.3 分库分表

分库:单个服务器的磁盘存储容量和CPU处理能力是有极限的,随着存储数据量的增大或请求并发数的增多,单个服务器将无法满足需求。此时可以将原本存储在一个数据库中的数据分散到多个数据库实例中,通过分库来分散单个数据库的存储压力和访问请求,提高系统的可用性和并发处理能力。

分表:对于单张表,随着表中总记录行数的增大,底层B+树的层数也会增大,导致平均访问时延增大。此时可以将原本存储在一张表中的记录分散到多张表中,通过分表来减小单张表中的记录行数,提升访问速度。

1.2 @DS的使用流程

1.2.1 添加依赖

	<dependency>
	  <groupId>com.baomidou</groupId>
	  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
	  <version>${version}</version>
	</dependency>

1.2.2 配置数据源

spring:
  datasource:
    dynamic:
      primary: master
      strict: false
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx)
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver

1.2.3 使用 @DS 切换数据源

没有使用@DS时使用默认数据源(primary指定的数据源)。
@DS可以作用于Service实现类上或类中的方法上,参数值为指定数据源所配置的库名。


@Service
@DS("master")
public class UserServiceImpl implements UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List selectAll() {
        return jdbcTemplate.queryForList("select * from user");
    }

    @Override
    @DS("slave_1")
    public List selectByCondition() {
        return jdbcTemplate.queryForList("select * from user where age >10");
    }
}

1.3 使用@DS切换数据源失效

1.3.1 情形一:同类中的方法调用

@DS是基于AOP实现的,底层使用的是动态代理。同类中的方法调用属于直接方法调用,不走代理对象,所以不会触发代理逻辑,导致@DS失效。

简单的做法是让调用者和被调用者处于不同类中。另一种做法是在类中注入本类的代理对象,然后在调用本类中被增强了的方法时,使用代理对象来调用。

1.3.2 情形二:@Transactional标注的方法调用@DS标注的方法

@Transactional@DS都是基于AOP实现的,底层使用的是动态代理。@Transactional标注的方法调用@DS标注的方法时,若采用的是默认的事务传播行为,会导致@DS方法的数据源失效,使用的是外层的@Transactional方法的数据源。

解决办法是在内外方法都使用@Transactional,并且事务的传播行为都选择为Propagation.REQUIRES_NEW,此时内层方法上的@DS可以生效。

二、分布式锁:@DistributeLock

2.1 分布式锁的作用

确保在分布式环境下对于某个资源或操作的并发访问能够按照预期的互斥原则执行。它帮助避免多线程或多服务实例间的并发冲突,保证数据的一致性和完整性。

2.2 @DistributeLock的关键参数

@DistributeLock(keyPrefix = "oms-again-order", keyName = "#req.orderCode + '_' + #req.userId", timeOutMSec = 5000, expireMSecs = 5000)

  • keyPrefix:键前缀,一般使用业务功能(一般也正好是方法名)作为键前缀。
  • keyName:键名,和键前缀一起作为锁的唯一标识符,一般使用业务相关的id。
  • timeOutMSec:获取锁超时时间,即尝试获取锁时能够等待的最长时间,避免因获取不到锁而长期阻塞,超时返回后可以报错或重试。
  • expireMSecs:锁的过期时间,无论是否完成了其保护的业务操作,锁都会在指定的毫秒数后自动失效并释放,避免因锁无法释放导致进死锁。

2.3 分布式锁的使用场景

  • 分布式环境下的资源竞争问题。
  • 请求重复提交问题。

三、分布式事务:@GlobalTransactional

2.1 分布式事务的作用

分布式事务是指跨越多个分布式系统或服务的事务操作,需要保证这些操作作为一个整体,要么全部成功,要么全部失败,以此来维护数据的一致性和完整性。

2.2 @GlobalTransactional的关键参数

@GlobalTransactional(name = "oms-again-order", rollbackFor = Exception.class)

  • name:全局事务名称,用于事务的识别和跟踪。
  • rollbackFor:回滚条件,一般为Exception.class或Throwable.class。
  • timeoutMills:设置全局事务的超时时间(毫秒)。如果事务在这段时间内没有完成,将会被系统自动回滚。这个参数有助于防止事务长时间挂起,影响系统性能。

2.3 @GlobalTransactional与@DistributeLock同时使用

并非所有业务都需要同时使用@DistributeLock@GlobalTransactional,前者用于解决并发环境下的互斥问题,后者用于实现一组操作的一致性和完整性。

需要同时使用时,建议@DistributeLock要在@GlobalTransactional之前使用(非必须)。这样会先获取到分布式锁后再开启分布式事务。如果顺序相反,那么会先开发分布式事务再获取分布式锁,迟迟没有获取到锁,会当分布式事务一致等待,可能导致分布式事务超时。

2.4 @GlobalTransactional原理

全局事务由多个本地事务和一张表实现,其中表记录的是已提交的本地事务的XID。若所有的本地事务都成功提交,那么全局事务也成功提交了;若存在一个本地事务失败,那么全局事务就会根据表来查询出已提交的本地事务,将这些事务回滚,从而实现全局事务的回滚。

参考链接

Dynamic-Datasource官网
@DS注解在事务中实现数据源的切换 / @DS在事务中失效

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值