spring boot原理,springboot aop使用_SpringBoot 使用AOP實現讀寫分離

 2023-10-05 阅读 27 评论 0

摘要:前言????入職新公司到現在也有一個月了,完成了手頭的工作,前幾天終于有時間研究下公司舊項目的代碼。在研究代碼的過程中,發現項目里用到了Spring Aop來實現數據庫的讀寫分離,本著自己愛學習(我自己都不信…)的性格,決定寫個實例工程來實

前言

????入職新公司到現在也有一個月了,完成了手頭的工作,前幾天終于有時間研究下公司舊項目的代碼。在研究代碼的過程中,發現項目里用到了Spring Aop來實現數據庫的讀寫分離,本著自己愛學習(我自己都不信…)的性格,決定寫個實例工程來實現spring aop讀寫分離的效果。

環境部署

  • 數據庫:MySql

  • 庫數量:2個,一主一從

spring boot原理,關于mysql的主從環境部署,可以參考:

https://juejin.im/post/5dd13778e51d453da86c0e6f

開始項目

首先,毫無疑問,先開始搭建一個SpringBoot工程,然后在pom文件中引入如下依賴:

<dependencies>
????????<dependency>
????????????<groupId>com.alibabagroupId>
????????????<artifactId>druid-spring-boot-starterartifactId>
????????????<version>1.1.10version>
????????dependency>
????????<dependency>
????????????<groupId>org.mybatis.spring.bootgroupId>
????????????<artifactId>mybatis-spring-boot-starterartifactId>
????????????<version>1.3.2version>
????????dependency>
????????<dependency>
????????????<groupId>tk.mybatisgroupId>
????????????<artifactId>mapper-spring-boot-starterartifactId>
????????????<version>2.1.5version>
????????dependency>
????????<dependency>
????????????<groupId>mysqlgroupId>
????????????<artifactId>mysql-connector-javaartifactId>
????????????<version>8.0.16version>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-jdbcartifactId>
????????????<scope>providedscope>
????????dependency>
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-aopartifactId>
????????????<scope>providedscope>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-webartifactId>
????????dependency>
????????<dependency>
????????????<groupId>org.projectlombokgroupId>
????????????<artifactId>lombokartifactId>
????????????<optional>trueoptional>
????????dependency>
????????<dependency>
????????????<groupId>com.alibabagroupId>
????????????<artifactId>fastjsonartifactId>
????????????<version>1.2.4version>
????????dependency>
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-testartifactId>
????????????<scope>testscope>
????????dependency>
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-data-jpaartifactId>
????????dependency>
????dependencies>

目錄結構

引入基本的依賴后,整理一下目錄結構,完成后的項目骨架大致如下:

Springboot項目。a7fbffef0aec925ca81f8ed87f71f838.png

建表

創建一張表user,在主庫執行sql語句同時在從庫生成對應的表數據

DROP?TABLE?IF?EXISTS?`user`;
CREATE?TABLE?`user`?(
??`user_id`?bigint(20)?NOT?NULL?COMMENT?'用戶id',
??`user_name`?varchar(255)?DEFAULT?''?COMMENT?'用戶名稱',
??`user_phone`?varchar(50)?DEFAULT?''?COMMENT?'用戶手機',
??`address`?varchar(255)?DEFAULT?''?COMMENT?'住址',
??`weight`?int(3)?NOT?NULL?DEFAULT?'1'?COMMENT?'權重,大者優先',
??`created_at`?datetime?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?COMMENT?'創建時間',
??`updated_at`?datetime?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP?COMMENT?'更新時間',
??PRIMARY?KEY?(`user_id`)
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8;

INSERT?INTO?`user`?VALUES?('1196978513958141952',?'測試1',?'18826334748',?'廣州市海珠區',?'1',?'2019-11-20?10:28:51',?'2019-11-22?14:28:26');
INSERT?INTO?`user`?VALUES?('1196978513958141953',?'測試2',?'18826274230',?'廣州市天河區',?'2',?'2019-11-20?10:29:37',?'2019-11-22?14:28:14');
INSERT?INTO?`user`?VALUES?('1196978513958141954',?'測試3',?'18826273900',?'廣州市天河區',?'1',?'2019-11-20?10:30:19',?'2019-11-22?14:28:30');

主從數據源配置

application.yml,主要信息是主從庫的數據源配置

server:
??port:?8001
spring:
??jackson:
??????date-format:?yyyy-MM-dd?HH:mm:ss
??????time-zone:?GMT+8
??datasource:
????type:?com.alibaba.druid.pool.DruidDataSource
????driver-class-name:?com.mysql.cj.jdbc.Driver
????master:
??????url:?jdbc:mysql://127.0.0.1:3307/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
??????username:?root
??????password:
????slave:
??????url:?jdbc:mysql://127.0.0.1:3308/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
??????username:?root
??????password:

因為有一主一從兩個數據源,我們用枚舉類來代替,方便我們使用時能對應

@Getter
public?enum?DynamicDataSourceEnum?{
????MASTER("master"),
????SLAVE("slave");
????private?String?dataSourceName;
????DynamicDataSourceEnum(String?dataSourceName)?{
????????this.dataSourceName?=?dataSourceName;
????}
}

java讀寫分離、數據源配置信息類 DataSourceConfig,這里配置了兩個數據源,masterDb和slaveDb

@Configuration
@MapperScan(basePackages?=?"com.xjt.proxy.mapper",?sqlSessionTemplateRef?=?"sqlTemplate")
public?class?DataSourceConfig?{

?????//?主庫
??????@Bean
??????@ConfigurationProperties(prefix?=?"spring.datasource.master")
??????public?DataSource?masterDb()?{
??return?DruidDataSourceBuilder.create().build();
??????}

????/**
?????*?從庫
?????*/
????@Bean
????@ConditionalOnProperty(prefix?=?"spring.datasource",?name?=?"slave",?matchIfMissing?=?true)
????@ConfigurationProperties(prefix?=?"spring.datasource.slave")
????public?DataSource?slaveDb()?{
????????return?DruidDataSourceBuilder.create().build();
????}

????/**
?????*?主從動態配置
?????*/
????@Bean
????public?DynamicDataSource?dynamicDb(@Qualifier("masterDb")?DataSource?masterDataSource,
????????@Autowired(required?=?false)?@Qualifier("slaveDb")?DataSource?slaveDataSource)?{
????????DynamicDataSource?dynamicDataSource?=?new?DynamicDataSource();
????????Map?targetDataSources?=?new?HashMap<>();
????????targetDataSources.put(DynamicDataSourceEnum.MASTER.getDataSourceName(),?masterDataSource);if?(slaveDataSource?!=?null)?{
????????????targetDataSources.put(DynamicDataSourceEnum.SLAVE.getDataSourceName(),?slaveDataSource);
????????}
????????dynamicDataSource.setTargetDataSources(targetDataSources);
????????dynamicDataSource.setDefaultTargetDataSource(masterDataSource);return?dynamicDataSource;
????}@Beanpublic?SqlSessionFactory?sessionFactory(@Qualifier("dynamicDb")?DataSource?dynamicDataSource)?throws?Exception?{
????????SqlSessionFactoryBean?bean?=?new?SqlSessionFactoryBean();
????????bean.setMapperLocations(
????????????new?PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
????????bean.setDataSource(dynamicDataSource);return?bean.getObject();
????}@Beanpublic?SqlSessionTemplate?sqlTemplate(@Qualifier("sessionFactory")?SqlSessionFactory?sqlSessionFactory)?{return?new?SqlSessionTemplate(sqlSessionFactory);
????}@Bean(name?=?"dataSourceTx")public?DataSourceTransactionManager?dataSourceTx(@Qualifier("dynamicDb")?DataSource?dynamicDataSource)?{
????????DataSourceTransactionManager?dataSourceTransactionManager?=?new?DataSourceTransactionManager();
????????dataSourceTransactionManager.setDataSource(dynamicDataSource);return?dataSourceTransactionManager;
????}
}

設置路由

設置路由的目的為了方便查找對應的數據源,我們可以用ThreadLocal保存數據源的信息到每個線程中,方便我們需要時獲取

public?class?DataSourceContextHolder?{
????private?static?final?ThreadLocal?DYNAMIC_DATASOURCE_CONTEXT?=?new?ThreadLocal<>();public?static?void?set(String?datasourceType)?{
????????DYNAMIC_DATASOURCE_CONTEXT.set(datasourceType);
????}public?static?String?get()?{return?DYNAMIC_DATASOURCE_CONTEXT.get();
????}public?static?void?clear()?{
????????DYNAMIC_DATASOURCE_CONTEXT.remove();
????}
}

獲取路由

public?class?DynamicDataSource?extends?AbstractRoutingDataSource?{
????@Override
????protected?Object?determineCurrentLookupKey()?{
????????return?DataSourceContextHolder.get();
????}
}

AbstractRoutingDataSource的作用是基于查找key路由到對應的數據源,它內部維護了一組目標數據源,并且做了路由key與目標數據源之間的映射,提供基于key查找數據源的方法。更多springboot文章,查看往期:SpringBoot內容聚合

數據源的注解

Springboot常用注解、為了可以方便切換數據源,我們可以寫一個注解,注解中包含數據源對應的枚舉值,默認是主庫,

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public?@interface?DataSourceSelector?{

????DynamicDataSourceEnum?value()?default?DynamicDataSourceEnum.MASTER;
????boolean?clear()?default?true;
}

aop切換數據源

到這里,aop終于可以現身出場了,這里我們定義一個aop類,對有注解的方法做切換數據源的操作,具體代碼如下:

@Slf4j
@Aspect
@Order(value?=?1)
@Component
public?class?DataSourceContextAop?{

?@Around("@annotation(com.xjt.proxy.dynamicdatasource.DataSourceSelector)")
????public?Object?setDynamicDataSource(ProceedingJoinPoint?pjp)?throws?Throwable?{
????????boolean?clear?=?true;
????????try?{
????????????Method?method?=?this.getMethod(pjp);
????????????DataSourceSelector?dataSourceImport?=?method.getAnnotation(DataSourceSelector.class);
????????????clear?=?dataSourceImport.clear();
????????????DataSourceContextHolder.set(dataSourceImport.value().getDataSourceName());
????????????log.info("========數據源切換至:{}",?dataSourceImport.value().getDataSourceName());
????????????return?pjp.proceed();
????????}?finally?{
????????????if?(clear)?{
????????????????DataSourceContextHolder.clear();
????????????}

????????}
????}
????private?Method?getMethod(JoinPoint?pjp)?{
????????MethodSignature?signature?=?(MethodSignature)pjp.getSignature();
????????return?signature.getMethod();
????}

}

到這一步,我們的準備配置工作就完成了,下面開始測試效果。

先寫好Service文件,包含讀取和更新兩個方法,

@Service
public?class?UserService?{

????@Autowired
????private?UserMapper?userMapper;

????@DataSourceSelector(value?=?DynamicDataSourceEnum.SLAVE)
????public?List?listUser()?{
????????List?users?=?userMapper.selectAll();return?users;
????}@DataSourceSelector(value?=?DynamicDataSourceEnum.MASTER)public?int?update()?{
????????User?user?=?new?User();
????????user.setUserId(Long.parseLong("1196978513958141952"));
????????user.setUserName("修改后的名字2");return?userMapper.updateByPrimaryKeySelective(user);
????}@DataSourceSelector(value?=?DynamicDataSourceEnum.SLAVE)public?User?find()?{
????????User?user?=?new?User();
????????user.setUserId(Long.parseLong("1196978513958141952"));return?userMapper.selectByPrimaryKey(user);
????}
}

springboot注解、根據方法上的注解可以看出,讀的方法走從庫,更新的方法走主庫,更新的對象是userId為1196978513958141953 的數據,

然后我們寫個測試類測試下是否能達到效果,

@RunWith(SpringRunner.class)
@SpringBootTest
class?UserServiceTest?{

????@Autowired
????UserService?userService;

????@Test
????void?listUser()?{
????????List?users?=?userService.listUser();for?(User?user?:?users)?{
????????????System.out.println(user.getUserId());
????????????System.out.println(user.getUserName());
????????????System.out.println(user.getUserPhone());
????????}
????}@Test
????void?update()?{
????????userService.update();
????????User?user?=?userService.find();
????????System.out.println(user.getUserName());
????}
}

測試結果:

1、讀取方法

e70d8c0f88ff628010a5a22c6e39d667.png

springboot 多數據源、2、更新方法

b889166b8a54a5836a4af8da44b9dc53.png

執行之后,比對數據庫就可以發現主從庫都修改了數據,說明我們的讀寫分離是成功的。當然,更新方法可以指向從庫,這樣一來就只會修改到從庫的數據,而不會涉及到主庫。

注意

上面測試的例子雖然比較簡單,但也符合常規的讀寫分離配置。值得說明的是,讀寫分離的作用是為了緩解寫庫,也就是主庫的壓力,但一定要基于數據一致性的原則,就是保證主從庫之間的數據一定要一致。如果一個方法涉及到寫的邏輯,那么該方法里所有的數據庫操作都要走主庫。

springbootaop,假設寫的操作執行完后數據有可能還沒同步到從庫,然后讀的操作也開始執行了,如果這個讀取的程序走的依然是從庫的話,那么就會出現數據不一致的現象了,這是我們不允許的。
最后發一下項目的github地址,有興趣的同學可以看下:

https://github.com/Taoxj/mysql-proxy

參考:

https://www.cnblogs.com/cjsblog/p/9712457.html

出處:作者:鄙人薛某

juejin.im/post/5ddcd93af265da7dce3271de

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/114107.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息