<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          讀寫分離很難嗎?SpringBoot結(jié)合aop簡單就實現(xiàn)了

          共 9847字,需瀏覽 20分鐘

           ·

          2020-08-03 11:15



          來源?|?juejin.im/post/5ddcd93af265da7dce3271de

          前言

          入職新公司到現(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?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;
          ????}
          ????@Bean
          ????public?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();
          ????}
          ????@Bean
          ????public?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;
          ????}
          }

          設置路由

          設置路由的目的為了方便查找對應的數(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 解析集合配置

          瀏覽 43
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天堂综合在线 | 天天操天天日天天摸 | 黄色在线播放视频免费 | 国产精品人妻无码久久久郑州天气网 | 老鸭窝在线观看视频 |