讀寫分離很難嗎?SpringBoot結(jié)合aop簡單就實現(xiàn)了
前言
入職新公司到現(xiàn)在也有一個月了,完成了手頭的工作,前幾天終于有時間研究下公司舊項目的代碼。在研究代碼的過程中,發(fā)現(xiàn)項目里用到了Spring Aop來實現(xiàn)數(shù)據(jù)庫的讀寫分離,本著自己愛學習(我自己都不信…)的性格,決定寫個實例工程來實現(xiàn)spring aop讀寫分離的效果。
環(huán)境部署
數(shù)據(jù)庫:MySql
庫數(shù)量:2個,一主一從
關(guān)于mysql的主從環(huán)境部署,可以參考:
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>
目錄結(jié)構(gòu)
引入基本的依賴后,整理一下目錄結(jié)構(gòu),完成后的項目骨架大致如下:

建表
創(chuàng)建一張表user,在主庫執(zhí)行sql語句同時在從庫生成對應的表數(shù)據(jù)
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?'權(quán)重,大者優(yōu)先',
??`created_at`?datetime?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?COMMENT?'創(chuàng)建時間',
??`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',?'廣州市海珠區(qū)',?'1',?'2019-11-20?10:28:51',?'2019-11-22?14:28:26');
INSERT?INTO?`user`?VALUES?('1196978513958141953',?'測試2',?'18826274230',?'廣州市天河區(qū)',?'2',?'2019-11-20?10:29:37',?'2019-11-22?14:28:14');
INSERT?INTO?`user`?VALUES?('1196978513958141954',?'測試3',?'18826273900',?'廣州市天河區(qū)',?'1',?'2019-11-20?10:30:19',?'2019-11-22?14:28:30');
主從數(shù)據(jù)源配置
application.yml,主要信息是主從庫的數(shù)據(jù)源配置
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:
因為有一主一從兩個數(shù)據(jù)源,我們用枚舉類來代替,方便我們使用時能對應
@Getter
public?enum?DynamicDataSourceEnum?{
????MASTER("master"),
????SLAVE("slave");
????private?String?dataSourceName;
????DynamicDataSourceEnum(String?dataSourceName)?{
????????this.dataSourceName?=?dataSourceName;
????}
}
數(shù)據(jù)源配置信息類 DataSourceConfig,這里配置了兩個數(shù)據(jù)源,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();
????}
????/**
?????*?主從動態(tài)配置
?????*/
????@Bean
????public?DynamicDataSource?dynamicDb(@Qualifier("masterDb")?DataSource?masterDataSource,
????????@Autowired(required?=?false)?@Qualifier("slaveDb")?DataSource?slaveDataSource)?{
????????DynamicDataSource?dynamicDataSource?=?new?DynamicDataSource();
????????Map設置路由
設置路由的目的為了方便查找對應的數(shù)據(jù)源,我們可以用ThreadLocal保存數(shù)據(jù)源的信息到每個線程中,方便我們需要時獲取
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路由到對應的數(shù)據(jù)源,它內(nèi)部維護了一組目標數(shù)據(jù)源,并且做了路由key與目標數(shù)據(jù)源之間的映射,提供基于key查找數(shù)據(jù)源的方法。更多springboot文章
數(shù)據(jù)源的注解
為了可以方便切換數(shù)據(jù)源,我們可以寫一個注解,注解中包含數(shù)據(jù)源對應的枚舉值,默認是主庫,
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public?@interface?DataSourceSelector?{
????DynamicDataSourceEnum?value()?default?DynamicDataSourceEnum.MASTER;
????boolean?clear()?default?true;
}
aop切換數(shù)據(jù)源
到這里,aop終于可以現(xiàn)身出場了,這里我們定義一個aop類,對有注解的方法做切換數(shù)據(jù)源的操作,具體代碼如下:
@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("========數(shù)據(jù)源切換至:{}",?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);
????}
}
根據(jù)方法上的注解可以看出,讀的方法走從庫,更新的方法走主庫,更新的對象是userId為1196978513958141953 的數(shù)據(jù),
然后我們寫個測試類測試下是否能達到效果,
@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());
????}
}
測試結(jié)果:
1、讀取方法

2、更新方法

執(zhí)行之后,比對數(shù)據(jù)庫就可以發(fā)現(xiàn)主從庫都修改了數(shù)據(jù),說明我們的讀寫分離是成功的。當然,更新方法可以指向從庫,這樣一來就只會修改到從庫的數(shù)據(jù),而不會涉及到主庫。
注意
上面測試的例子雖然比較簡單,但也符合常規(guī)的讀寫分離配置。值得說明的是,讀寫分離的作用是為了緩解寫庫,也就是主庫的壓力,但一定要基于數(shù)據(jù)一致性的原則,就是保證主從庫之間的數(shù)據(jù)一定要一致。如果一個方法涉及到寫的邏輯,那么該方法里所有的數(shù)據(jù)庫操作都要走主庫。
假設寫的操作執(zhí)行完后數(shù)據(jù)有可能還沒同步到從庫,然后讀的操作也開始執(zhí)行了,如果這個讀取的程序走的依然是從庫的話,那么就會出現(xiàn)數(shù)據(jù)不一致的現(xiàn)象了,這是我們不允許的。
最后發(fā)一下項目的github地址,有興趣的同學可以看下:
https://github.com/Taoxj/mysql-proxy
參考:
https://www.cnblogs.com/cjsblog/p/9712457.html
推薦閱讀:
實戰(zhàn):一鍵生成前后端代碼,Mybatis-Plus代碼生成器讓我舒服了 為什么要重寫 hashcode 和 equals 方法? SpringBoot @Value 解析集合配置
