javascript
简单好用!利用Spring AOP技术10分钟实现一个读写分离方案
點擊上方?好好學java?,選擇?星標?公眾號
重磅資訊、干貨,第一時間送達今日推薦:2020年7月程序員工資統計,平均14357元,又跌了,扎心個人原創100W+訪問量博客:點擊前往,查看更多作者:鄙人薛某
juejin.im/post/5ddcd93af265da7dce3271de
前言
入職新公司到現在也有一個月了,完成了手頭的工作,前幾天終于有時間研究下公司舊項目的代碼。在研究代碼的過程中,發現項目里用到了Spring Aop來實現數據庫的讀寫分離,本著自己愛學習(我自己都不信…)的性格,決定寫個實例工程來實現spring aop讀寫分離的效果。
環境部署
數據庫:MySql
庫數量:2個,一主一從
關于mysql的主從環境部署,可以參考:
https://juejin.im/post/5dd13778e51d453da86c0e6f
開始項目
首先,毫無疑問,先開始搭建一個SpringBoot工程,然后在pom文件中引入如下依賴:
<dependencies><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>2.1.5</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.16</version></dependency><!--?動態數據源?所需依賴?###?start--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><scope>provided</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><scope>provided</scope></dependency><!--?動態數據源?所需依賴?###?end--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.4</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency></dependencies>目錄結構
引入基本的依賴后,整理一下目錄結構,完成后的項目骨架大致如下:
建表
創建一張表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:sstime-zone:?GMT+8datasource:type:?com.alibaba.druid.pool.DruidDataSourcedriver-class-name:?com.mysql.cj.jdbc.Drivermaster: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=trueusername:?rootpassword: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=trueusername:?rootpassword:因為有一主一從兩個數據源,我們用枚舉類來代替,方便我們使用時能對應
@Getter public?enum?DynamicDataSourceEnum?{MASTER("master"),SLAVE("slave");private?String?dataSourceName;DynamicDataSourceEnum(String?dataSourceName)?{this.dataSourceName?=?dataSourceName;} }數據源配置信息類 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();}/***?主從動態配置*/@Beanpublic?DynamicDataSource?dynamicDb(@Qualifier("masterDb")?DataSource?masterDataSource,@Autowired(required?=?false)?@Qualifier("slaveDb")?DataSource?slaveDataSource)?{DynamicDataSource?dynamicDataSource?=?new?DynamicDataSource();Map<Object,?Object>?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<String>?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?{@Overrideprotected?Object?determineCurrentLookupKey()?{return?DataSourceContextHolder.get();} }AbstractRoutingDataSource的作用是基于查找key路由到對應的數據源,它內部維護了一組目標數據源,并且做了路由key與目標數據源之間的映射,提供基于key查找數據源的方法。更多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();}}到這一步,我們的準備配置工作就完成了,下面開始測試效果。更多springboot文章,查看往期:SpringBoot內容聚合
先寫好Service文件,包含讀取和更新兩個方法,
@Service public?class?UserService?{@Autowiredprivate?UserMapper?userMapper;@DataSourceSelector(value?=?DynamicDataSourceEnum.SLAVE)public?List<User>?listUser()?{List<User>?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);} }根據方法上的注解可以看出,讀的方法走從庫,更新的方法走主庫,更新的對象是userId為1196978513958141953 的數據,
然后我們寫個測試類測試下是否能達到效果,
@RunWith(SpringRunner.class) @SpringBootTest class?UserServiceTest?{@AutowiredUserService?userService;@Testvoid?listUser()?{List<User>?users?=?userService.listUser();for?(User?user?:?users)?{System.out.println(user.getUserId());System.out.println(user.getUserName());System.out.println(user.getUserPhone());}}@Testvoid?update()?{userService.update();User?user?=?userService.find();System.out.println(user.getUserName());} }測試結果:
1、讀取方法
2、更新方法
執行之后,比對數據庫就可以發現主從庫都修改了數據,說明我們的讀寫分離是成功的。當然,更新方法可以指向從庫,這樣一來就只會修改到從庫的數據,而不會涉及到主庫。
注意
上面測試的例子雖然比較簡單,但也符合常規的讀寫分離配置。值得說明的是,讀寫分離的作用是為了緩解寫庫,也就是主庫的壓力,但一定要基于數據一致性的原則,就是保證主從庫之間的數據一定要一致。如果一個方法涉及到寫的邏輯,那么該方法里所有的數據庫操作都要走主庫。
假設寫的操作執行完后數據有可能還沒同步到從庫,然后讀的操作也開始執行了,如果這個讀取的程序走的依然是從庫的話,那么就會出現數據不一致的現象了,這是我們不允許的。
最后發一下項目的github地址,有興趣的同學可以看下:
https://github.com/Taoxj/mysql-proxy
參考:
https://www.cnblogs.com/cjsblog/p/9712457.html
最后,再附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,筆者這幾年及春招的總結,github 1.4k star,拿去不謝!
下載方式
1.?首先掃描下方二維碼
2.?后臺回復「Java面試」即可獲取
總結
以上是生活随笔為你收集整理的简单好用!利用Spring AOP技术10分钟实现一个读写分离方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 这么写注释,老板会不会开除我?
- 下一篇: 2行代码实现小程序分享到朋友圈功能