使用springboot aop来实现读写分离和事物配置

作者:Coder_Qiang 时间:2022-10-29 11:43:07 

什么事读写分离

读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。

为什么要实现读写分离

增加冗余

增加了机器的处理能力

对于读操作为主的应用,使用读写分离是最好的场景,因为可以确保写的服务器压力更小,而读又可以接受点时间上的延迟。

实现

本文介绍利用spring aop来动态切换数据源来实现读写分离。

先建一个maven项目,导入springBoot依赖。


<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
</parent>

<dependencies>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis-->
<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
 <artifactId>mybatis-spring-boot-starter</artifactId>
 <version>1.3.1</version>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- druid -->
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid</artifactId>
 <version>${druid.version}</version>
</dependency>
<!-- mysql connector-->
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>${mysql-connector.version}</version>
</dependency>

<!--test-->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
</dependency>

</dependencies>

然后在配置文件application.yml中自定义数据源配置项


server:
port: 8080
logging:
level:
org.springframework: INFO
com.qiang: DEBUG
spring:
output:
ansi:
 enabled: always
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_area?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
db:
readsize: 2
read0:
 type: com.alibaba.druid.pool.DruidDataSource
 driver-class-name: com.mysql.cj.jdbc.Driver
 url: jdbc:mysql://localhost:3306/db_area?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
 username: root
 password: root

read1:
 type: com.alibaba.druid.pool.DruidDataSource
 driver-class-name: com.mysql.cj.jdbc.Driver
 url: jdbc:mysql://localhost:3306/db_area?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
 username: root
 password: root
aop:
auto: true
proxy-target-class: true

配置Druid


package com.qiang.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
* @author gengqiang
* @date 2018/5/3
*/
@Configuration
public class DruidConfig {

private Logger logger = LoggerFactory.getLogger(DruidConfig.class

/**
 * 主据源
 * @return
 */
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
 return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();

}

/**
 * 从数据源1
 * @return
 */
@Bean(name = "readDataSource0")
@ConfigurationProperties(prefix = "spring.db.read0")
public DataSource readDataSource0() {
 return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();

}
/**
 * 从数据源2
 * @return
 */
@Bean(name = "readDataSource1")
@ConfigurationProperties(prefix = "spring.db.read1")
public DataSource readDataSource1() {
 return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();

}

}

配置Mybaits


package com.qiang.config;

import com.qiang.config.db.DataSourceType;
import com.qiang.config.db.RoutingDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* @author gengqiang
* @date 2018/5/3
*/
@Configuration
@EnableTransactionManagement(order = 2)
@MapperScan(basePackages = {"com.qiang.demo.mapper"})
public class MybatisConfig implements TransactionManagementConfigurer, ApplicationContextAware {

private static ApplicationContext context;

/**
 * 写库数据源
 */
@Autowired
private DataSource dataSource;

/**
 * 读数据源数量
 */
@Value("${spring.db.readsize}")
private Integer readsize;
/**
 * 数据源路由代理
 *
 * @return
 */

@Bean
public AbstractRoutingDataSource routingDataSouceProxy() {
 RoutingDataSource proxy = new RoutingDataSource(readsize);
 Map<Object, Object> targetDataSources = new HashMap<>(readsize + 1);
 targetDataSources.put(DataSourceType.WRITE.getType(), dataSource);
 for (int i = 0; i < readsize; i++) {
  DataSource d = context.getBean("readDataSource" + i, DataSource.class);
  targetDataSources.put(i, d);
 }
 proxy.setDefaultTargetDataSource(dataSource);
 proxy.setTargetDataSources(targetDataSources);
 return proxy;
}

@Bean
@ConditionalOnMissingBean
public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
 SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
 bean.setDataSource(routingDataSouceProxy());
 bean.setVfs(SpringBootVFS.class);
 bean.setTypeAliasesPackage("com.qiang");
 Resource configResource = new ClassPathResource("/mybatis-config.xml");
 bean.setConfigLocation(configResource);
 ResourcePatternResolver mapperResource = new PathMatchingResourcePatternResolver();
 bean.setMapperLocations(mapperResource.getResources("classpath*:mapper/**/*.xml"));
 return bean;
}

@Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
 return new DataSourceTransactionManager(routingDataSouceProxy());
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 if (context == null) {
  context = applicationContext;
 }
}
}

其中实现数据源切换的功能就是自定义一个类扩展AbstractRoutingDataSource抽象类,就是代码中的定义的RoutingDataSource,其实该相当于数据源DataSourcer的路由中介,可以实现在项目运行时根据相应key值切换到对应的数据源DataSource上。

RoutingDataSource.class


package com.qiang.config.db;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.concurrent.atomic.AtomicInteger;

/**
* 数据源路由
*
* @author gengqiang
*/
public class RoutingDataSource extends AbstractRoutingDataSource {

private AtomicInteger count = new AtomicInteger(0);

private int readsize;

public RoutingDataSource(int readsize) {
 this.readsize = readsize;
}

@Override
protected Object determineCurrentLookupKey() {
 String typeKey = DataSourceContextHolder.getJdbcType();
 if (typeKey == null) {
  logger.error("无法确定数据源");
 }
 if (typeKey.equals(DataSourceType.WRITE.getType())) {
  return DataSourceType.WRITE.getType();
 }
 //读库进行负载均衡
 int a = count.getAndAdd(1);
 int lookupkey = a % readsize;
 return lookupkey;
}

}

其中用到了2个辅助类


package com.qiang.config.db;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 全局数据源
*
* @author gengqiang
*/
public class DataSourceContextHolder {

private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

private final static ThreadLocal<String> local = new ThreadLocal<>();

public static ThreadLocal<String> getLocal() {
 return local;
}

public static void read() {
 logger.debug("切换至[读]数据源");
 local.set(DataSourceType.READ.getType());
}

public static void write() {
 logger.debug("切换至[写]数据源");
 local.set(DataSourceType.WRITE.getType());
}

public static String getJdbcType() {
 return local.get();
}

}

package com.qiang.config.db;

/**
* @author gengqiang
*/
public enum DataSourceType {
READ("read", "读库"), WRITE("write", "写库");
private String type;
private String name;

DataSourceType(String type, String name) {
 this.type = type;
 this.name = name;
}

public String getType() {
 return type;
}

public void setType(String type) {
 this.type = type;
}

public String getName() {
 return name;
}

public void setName(String name) {
 this.name = name;
}
}

最后通过aop设置切面,拦截读写来动态的设置数据源


package com.qiang.config.aop;

import com.qiang.config.db.DataSourceContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
* 拦截数据库读写
*
* @author gengqiang
*/

@Aspect
@Component
@Order(1)
public class DataSourceAspect {

Logger logger = LoggerFactory.getLogger(getClass());

@Before("execution(* com.qiang..*.*ServiceImpl.find*(..)) " +
  "|| execution(* com.qiang..*.*ServiceImpl.count*(..))" +
  "|| execution(* com.qiang..*.*ServiceImpl.sel*(..))" +
  "|| execution(* com.qiang..*.*ServiceImpl.get*(..))"
)
public void setReadDataSourceType() {
 logger.debug("拦截[read]方法");
 DataSourceContextHolder.read();
}

@Before("execution(* com.qiang..*.*ServiceImpl.insert*(..)) " +
  "|| execution(* com.qiang..*.*ServiceImpl.save*(..))" +
  "|| execution(* com.qiang..*.*ServiceImpl.update*(..))" +
  "|| execution(* com.qiang..*.*ServiceImpl.set*(..))" +
  "|| execution(* com.qiang..*.*ServiceImpl.del*(..))")
public void setWriteDataSourceType() {
 logger.debug("拦截[write]操作");
 DataSourceContextHolder.write();
}

}

主要的代码就写好了,下面来测试一下是否读写分离。

写一个测试类:


package com.qiang;

import com.qiang.demo.entity.Area;
import com.qiang.demo.service.AreaService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
* @author gengqiang
* @date 2018/5/4
*/

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestApplication {

@Autowired
private AreaService areaService;

@Test
public void test() {
 Area area = new Area();
 area.setDistrictId("0000");
 area.setName("test");
 area.setParentId(0);
 area.setLevel(1);
 areaService.insert(area);
}

@Test
public void test2() {
 areaService.selectByPrimaryKey(1);
}
}

其中第一个测试插入数据,第二个测试查询。

第一测试结果:

使用springboot aop来实现读写分离和事物配置

第二个测结果:

使用springboot aop来实现读写分离和事物配置

从结果看出来第一个走的写数据源,就是主数据源,第二个的走读数据源,就是从数据源。

然后我们在测试一下事物,看遇到异常是否会滚。

测试:


@Test
public void contextLoads() throws Exception {
 try {
  areaService.insertBack();

} catch (Exception e) {
//   e.printStackTrace();
 }
 System.out.println(areaService.count(new Area()));

}

其中service:


@Override
@Transactional(rollbackFor = Exception.class)
public void insertBack() {
Area area = new Area();
area.setDistrictId("0000");
area.setName("test");
area.setParentId(0);
area.setLevel(1);
mapper.insert(area);
throw new RuntimeException();
}

方法上加@Transactional,声明一个事物。

使用springboot aop来实现读写分离和事物配置

看一下运行结果,虽然运行插入的时候,sql是运行了,但最后查询的时候数量为0,说明会滚了。

配置事物

使用springboot aop来实现读写分离和事物配置

第一步需要加一个注解@EnableTransactionManagement,后面的参数是为了区分aop和事物执行的顺序。

然后在需要会滚的方法上加一个注解@Transactional。

源代码地址

来源:https://blog.csdn.net/Coder_Qiang/article/details/80205784

标签:springboot,aop,读写,配置
0
投稿

猜你喜欢

  • Java有哪些操作字符串的类?区别在哪?

    2021-06-02 14:50:54
  • Android传感器SensorEventListener之加速度传感器

    2023-03-07 15:05:50
  • java 图片验证码的实现代码

    2023-11-09 13:33:52
  • IDEA中使用jclasslib插件可视化方式查看类字节码的过程详解

    2021-10-12 08:18:31
  • Java安全框架——Shiro的使用详解(附springboot整合Shiro的demo)

    2022-05-29 09:46:46
  • C#中的yield关键字详解

    2023-01-11 08:19:01
  • 解决Java执行Cmd命令出现的死锁问题

    2023-06-15 09:54:56
  • Spring Boot利用Docker快速部署项目的完整步骤

    2022-03-08 18:52:55
  • MyBatis中使用$和#所遇到的问题及解决办法

    2023-03-25 12:27:55
  • C语言函数封装及变量的作用域

    2022-10-06 10:14:47
  • 基于Java接口回调详解

    2023-11-09 00:03:11
  • C#实现炫酷启动图-动态进度条效果

    2022-01-04 22:22:10
  • C# 表达式目录树Expression的实现

    2023-04-03 22:57:32
  • 关于Java利用反射实现动态运行一行或多行代码

    2021-11-27 23:12:55
  • 基于Jenkins搭建.NET Core持续集成环境过程图解

    2021-10-25 03:44:33
  • 详解JAVA动态代理

    2023-11-24 22:52:04
  • 浅析SpringCloud Alibaba-Nacos 作为注册中心示例代码

    2022-12-04 14:24:00
  • Android实现多段颜色进度条效果

    2023-09-30 22:07:51
  • 10张图总结出并发编程最佳学习路线

    2021-06-03 00:40:16
  • 浅谈一下单体架构的缺点是什么

    2022-07-16 06:20:12
  • asp之家 软件编程 m.aspxhome.com