需求与问题
最近,因为某一个项目需求的数据量过大,已经对其它服务造成了一定的影响。所以为了给数据库减负,将这部分业务需求涉及到的数据给迁到新库中。但这就出现了个问题,这部分业务涉及到的业务表我虽然迁到新库中,但涉及到一些扩展类的业务表,比如日志信息等,因为监控的是整个系统的日志,所以肯定不能把这个表给单独拿出来。所以,这里就涉及到了双数据源的访问。并且在一个方法中涉及到双数据源问题,切换以及对应的事务信息怎么解决。
数据源配置
spring:
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
datasource:
dynamic:
primary: master
druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
initial-size: 5
max-active: 20
min-idle: 5
max-wait: 60000
time-between-eviction-runs-millis: 50000
min-evictable-idle-time-millis: 120000
max-evictable-idle-time-millis: 280000
test-while-idle: true
test-on-borrow: false
test-on-return: false
validation-query: SELECT 1
keep-alive: true
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,slf4j # 注意这个值和druid原生不一致,默认启动了stat,wall
datasource:
master:
url: jdbc:p6spy:mysql://172.16.15.92:3306/questionnaire_audit?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&connectTimeout=6000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
username: airita
# @s#wwsOPw$RF6tfc%e
password: ENC(u2iKA7ejQVSQW32OvNAcErwYiXI2RjacdCTGli7PjxQ=)
ds2:
url: jdbc:p6spy:mysql://172.16.15.92:3306/airita?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&connectTimeout=6000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
username: airita
password: ENC(u2iKA7ejQVSQW32OvNAcErwYiXI2RjacdCTGli7PjxQ=)
jasypt:
encryptor:
password: airita
解决方案
个人在思考怎么解决这方面的问题时,想的解决方案。现在切换数据源的操作,我就用baomidou的@DS注解来做了。后续主要目的是为了解决事务与@DS注解的兼容问题,自身设想的方案为mysql触发器,spring事务隔离级别,以及@DSTransaction注解三种方案。最后采用了最后一种。
mysql触发器方案
既然问题是双数据源造成的,那么我把涉及到的扩展类业务表(这类业务表不多,而且只有查询方法)通过触发器的方式,实时同步到新库中,这样,在一个方法中,我访问的都是单独一个数据源的数据。那么@Transaction事务,不就没问题了?
- 创建sys_user触发器
create table questionnaire_audit.sys_user like sys_user;
insert into questionnaire_audit.sys_user
select * from sys_user;
CREATE TRIGGER sys_user_insert AFTER INSERT ON sys_user FOR EACH ROW
BEGIN
-- 插入目标表
INSERT INTO
questionnaire_audit.sys_user(user_id,login_id,password,org_id,user_name,user_sex,user_mobile,user_mail,create_time,update_time,user_state,comments,incharge_flag,source,accessMethod,userCreateType,userAuditStatus,userOrg,secretKey,foura_status,qyzx_login_id,qyzx_status)
VALUES
(new.user_id,new.login_id,new.password,new.org_id,new.user_name,new.user_sex,new.user_mobile,new.user_mail,new.create_time,new.update_time,new.user_state,new.comments,new.incharge_flag,new.source,new.accessMethod,new.userCreateType,new.userAuditStatus,new.userOrg,new.secretKey,new.foura_status,new.qyzx_login_id,new.qyzx_status);
END;
CREATE TRIGGER sys_user_delete AFTER DELETE ON sys_user FOR EACH ROW
BEGIN
-- INSE
DELETE FROM questionnaire_audit.sys_user WHERE user_id = old.user_id;
END;
CREATE TRIGGER sys_user_update AFTER UPDATE ON sys_user FOR EACH ROW
BEGIN
UPDATE questionnaire_audit.sys_user
SET user_id=new.user_id,login_id=new.login_id,password=new.password,org_id=new.org_id,user_name=new.user_name,user_sex=new.user_sex,user_mobile=new.user_mobile,user_mail=new.user_mail,create_time=new.create_time,update_time=new.update_time,user_state=new.user_state,comments=new.comments,incharge_flag=new.incharge_flag,source=new.source,accessMethod=new.accessMethod,userCreateType=new.userCreateType,userAuditStatus=new.userAuditStatus,userOrg=new.userOrg,secretKey=new.secretKey,foura_status=new.foura_status,qyzx_login_id=new.qyzx_login_id,qyzx_status=new.qyzx_status
WHERE user_id = new.user_id;
END;
对旧库的表来创建新建,修改,删除三方面的触发器。
后续想了下,这块的业务虽然很少改动,但后期也不敢保证完全不变。也就是说,后续我在新库要对扩展业务表想进行插入时,假设老库同时有大批量数据插入,会出问题吗?而且有的表是整个大项目通用的全量业务表,如果其它核心业务内容进行操作,那同步到我新库,我觉得也不合适。而且性能因为同步,所以性能也降低了太多。废弃。
这个其实我的目的就是练习一下触发器,以前也没用过。
Spring事务隔离级别
上面的解决方案是从数据库层面来解决的,该怎么说,太粗暴了。那么,既然事务出现的问题,那么我是否可以在事务层面来对这个问题进行解决?一个事务有问题,多个事务行不行?
@Transaction开的事务,可以理解为它是默认开启了主数据源的事务,在这个主事务的数据远源中,使用@DS注解然后对数据源进行切换,肯定是不行的。
Spring的事务隔离级别,默认情况下是require类型的事务。这种事务代表了,当外部存在事务时,内部方法存在事务,则需要将内部事务加入到外部事务中。而外部不存在事务时,内部事务自己新开一个事务来处理。也就是说,如果存在外部事务,内部事务与外部事务同属一个事务。这肯定不行的。
那么别的事务级别,require_new级别。当外部存在事务时,内部方法存在事务,则需要将内部事务新开一个事务处理。而外部不存在事务时,内部事务自己新开一个事务来处理。如果这么处理的话,把针对各自不同的数据源操作,放到不同的方法中,使用require_new开启新的事务,可以解决的。具体过程去网上查一下,很多文章的,只不过这种方法我没实践,后续也会练习一下。
不采用这种方式的原因有俩,一方面,将针对不同的数据源的操作存放到不同的方法中,设置对应的隔离级别。那么对业务的操作顺序有一定要求。另一方面,因为在不同的事务中,事务回滚该怎么办。这种的回滚方式,我后续也网上查下资料,并且实践下。
@DSTransaction注解
这个也是在网上找到的,看名字就知道了,和@DS注解同属baomidou一家公司的,不过包的版本要求是高版本。使用方法很简单,使用@DSTransaction直接替换了原先的@Transaction即可。
/**
* 语音转写结果回传地址
*/
@RequestMapping("/{version}/{identificationType}/returnService/inside")
@ResponseBody
//baomidou的DS与事务注解冲突
// @Transactional(rollbackFor = Exception.class)
@DSTransactional
public void transferReturnServiceInside(HttpServletRequest request, @PathVariable String version, @PathVariable String identificationType) {
}
感想
其实,学习和解决问题,我觉得我们还得有个系统的方式,就像这次的@DS注解一样,很明显同一家公司人家有配套的工具,我却一定要去网上乱查,说实在的,也是浪费时间。个人觉得,这也是大牛与菜鸡的一个区别,搜索问题,该怎么培养自己的思路呢?