zoukankan      html  css  js  c++  java
  • Spring Boot系列(三) Spring Boot 之 JDBC

    数据源

    类型

    • javax.sql.DataSource

    • javax.sql.XADataSource

    • org.springframework.jdbc.datasource.embedded,EnbeddedDataSource

    Spring Boot 中的数据源

    单数据源(官方推荐微服务使用单数据源)

    • 数据库连接池

      • Apache Commons DBCP

      • Tomcat DBCP

    多数据源

    实际生产中很可能会出现.

    事务

    • Spring 中的事务通过PlatformTransactionManagere 管理, 使用 AOP 实现, 具体实现类是 TransactionInterceptor.

    • 事务传播(propagation)与保护点(savepoint): 二者密切相关.

    Spring Boot 使用JDBC

    单数据源

    1. 配置:
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false
    spring.datasource.username=xlx
    spring.datasource.password=xlx
    

    注意serverTimezone的配置, 亚洲一般配置为 Hongkong.

    1. 访问类注入DataSource
    @Repository
    public class ProductRepository {
    
        @Autowired
        private DataSource dataSource;
        ...
    }
    

    多数据源配置

    1. 配置
    spring.ds1.name=master
    spring.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.ds1.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false
    spring.ds1.username=xlx
    spring.ds1.password=xlx
    
    spring.ds2.name=slave
    spring.ds2.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.ds2.url=jdbc:mysql://localhost:3306/product?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false
    spring.ds2.username=xlx
    spring.ds2.password=xlx
    
    1. 继承 AbstractRoutingDataSource 抽象类并实现方法determineCurrentLookupKey()

    这个类就是需要使用的数据源类型.

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

    查看 AbstractRoutingDataSource 的源码可以发现, 其定义了一个 Map<Object, Object> targetDataSources; 用来保存多数据源, 而上面重写的方法返回的就是需要使用的数据源的key.

    public enum DBType {
        MASTER,
        SLAVE
    }
    
    1. 定义 DbTypeContextHolder 其中使用一个 ThreadLocal 来保存key, 三个方法都必不可少. 分别用来获取, 设置, 清除.
    @Slf4j
    public class DbTypeContextHolder {
    
        private final static ThreadLocal<DBType> holder = new ThreadLocal<DBType>();
    
        private final static ThreadLocal<Boolean> alwaysWrite = new ThreadLocal<Boolean>();
    
        public static DBType get() {
            if (alwaysWrite.get() != null && alwaysWrite.get()) {
                log.info("将强制使用Write");
                return DBType.MASTER;
            }
            log.info("获取到: "+holder.get().name());
            return holder.get();
        }
    
        public static void set(DBType dbType) {
            log.info("设置为: "+dbType.name());
            holder.set(dbType);
        }
    
        public static void clean() {
            log.info("清除...");
            holder.remove();
        }
    
        public static void setAlwaysWrite() {
            log.info("设置强制Write");
            alwaysWrite.set(true);
        }
    
        public static void cleanAlwaysWrite() {
            log.info("清除强制Write");
            clean();
            alwaysWrite.remove();
        }
    
    }
    
    1. 定义数据源配置类 MultiDataSourceConfiguration
    @Configuration
    public class MultiDataSourceConfiguration {
    
        @Autowired
        Environment environment;
    
        @Bean
        public DataSource getDynamicDataSource(){
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            Map<Object,Object> map = new HashMap<>();
    
            for (int i = 1; i < 3 ; i++) {
                String name = environment.getProperty("spring.ds"+String.valueOf(i)+".name");
                if (name==null) break;
                DataSource dataSource = DataSourceBuilder.create()
                        .driverClassName(environment.getProperty("spring.ds"+String.valueOf(i)+".driver-class-name"))
                        .username(environment.getProperty("spring.ds"+String.valueOf(i)+".username"))
                        .url(environment.getProperty("spring.ds"+String.valueOf(i)+".url"))
                        .password(environment.getProperty("spring.ds"+String.valueOf(i)+".password"))
                        .build();
                if (name.trim().toUpperCase().equals("MASTER")){
                    dynamicDataSource.setDefaultTargetDataSource(dataSource);
                    map.put(DBType.MASTER,dataSource);
                }else{
                    map.put(DBType.SLAVE,dataSource);
                }
            }
    
            dynamicDataSource.setTargetDataSources(map);
            return dynamicDataSource;
        }
    }
    
    1. 检查是否可用
    public Boolean save(Product product) throws SQLException {
        System.out.println("save product : "+product);
        DbTypeContextHolder.set(DBType.MASTER);
        System.out.println(dataSource.getConnection());
        DbTypeContextHolder.clean();
    
        DbTypeContextHolder.set(DBType.SLAVE);
        System.out.println(dataSource.getConnection());
        DbTypeContextHolder.clean();
        return true;
    }
    

    结果如下,说明是两个不同的数据源:

    HikariProxyConnection@1667860231 wrapping com.mysql.cj.jdbc.ConnectionImpl@78794b01
    HikariProxyConnection@556437990 wrapping com.mysql.cj.jdbc.ConnectionImpl@314c0916
    
    1. 自动切换

    可以定义切面来自动切换数据源. 比如普通的读写分离.
    读写分离是通过定义在仓储层方法上的规则来实现, 同时也定义了在服务层必须使用写库来读取数据的方式.

    除此之外, 如果使用 @Transactional , 因为其也是通过AOP方式实现, 所以与自动切换的AOP存在顺序关系, 为了能在 @Transactional 事务前设置好数据源, 需要增加 @Order(0) .

    @Aspect
    @Component
    @Order(0)
    public class DataSourceAspect {
    
        // 必须使用写库的
        @Pointcut("execution(* com.xlx.product.repository.repo..*.save*(..)) || execution(* com.xlx.product.repository.repo..*.insert*(..)) || execution(* com.xlx.product.repository.repo..*.update*(..))")
        public void write(){}
    
        @Before("write()")
        public void beforeWrite(JoinPoint joinPoint){
            System.out.println("beforeWrite()"+joinPoint.getSignature().getName());
            DbTypeContextHolder.set(DBType.MASTER);
        }
    
        @After("write()")
        public void afterWrite(JoinPoint joinPoint){
            DbTypeContextHolder.clean();
        }
    
        // 必须使用读库的
        @Pointcut("execution(* com.xlx.product.repository.repo..*.get*(..))|| execution(* com.xlx.product.repository.repo..*.select*(..))||execution(* com.xlx.product.repository.repo..*.count*(..))")
        public void read(){}
    
        @Before("read()")
        public void beforeRead(JoinPoint joinPoint){
            DbTypeContextHolder.set(DBType.SLAVE);
        }
    
        @After("read()")
        public void afterRead(JoinPoint joinPoint){
            DbTypeContextHolder.clean();
        }
    
        // 服务层有注解的方法特殊处理
        @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)" +
                "||@annotation(com.xlx.product.repository.annotation.DBTypeAnnotation)")
        public void readWrite(){}
    
        @Before("readWrite()")
        public void before(JoinPoint joinPoint){
            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
            if(methodSignature.getMethod().isAnnotationPresent(Transactional.class)) DbTypeContextHolder.set(DBType.MASTER);
            if(methodSignature.getMethod().isAnnotationPresent(DBTypeAnnotation.class)) DbTypeContextHolder.setAlwaysWrite();
        }
    
        @After("readWrite()")
        public void after(JoinPoint joinPoint){
            DbTypeContextHolder.clean();
            DbTypeContextHolder.cleanAlwaysWrite();
        }
    
    }
    
    /**
     * 放置在方法上的注解, 用来指定使用的数据源类型, 默认使用主数据源
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DBTypeAnnotation {
    
        @AliasFor("values")
        DBType dbType() default DBType.MASTER;
    
        @AliasFor("dbType")
        DBType values() default DBType.MASTER;
    }
    
    1. service 和 controller
    @Service
    @Slf4j
    public class ProductService {
    
        @Autowired
        ProductRepository repository;
    
        public String findAndSave(){
            log.info("findAndSave() 方法开始.....");
            String rs = testFunc();
            log.info("findAndSave() 方法结束.....");
            return rs;
        }
    
        @DBTypeAnnotation
        public String findAndSaveWithWrite(){
            log.info("findAndSaveWithWrite() 方法开始.....");
            String rs = testFunc();
            log.info("findAndSaveWithWrite() 方法结束.....");
            return rs;
        }
    
        private String testFunc() {
            List<Product> productList = repository.selectAll();
            repository.saveProduct(productList.get(0));
            productList = repository.selectAll();
            repository.saveProduct(productList.get(0));
            return "ok";
        }
    
    }
    
    @RestController
    public class ProductController {
    
        @Autowired
        ProductService productService;
    
        @GetMapping("/product/findAndSave")
        public String findAndSave()throws SQLException {
            return productService.findAndSave();
        }
    
        @GetMapping("/product/findAndSaveWithWrite")
        public String findAndSaveWithWrite()throws SQLException {
            return productService.findAndSaveWithWrite();
        }
    }
    
  • 相关阅读:
    10_SpringBoot集成TkMybatis插件
    Mysql-YUM安装
    docker数据拷贝
    jquery实现倒计时功能
    CentOS 7.2使用源码包编译安装MySQL 5.7.22及一些操作
    vue中开发webSocket
    YARN 与Maprd 配置
    js实现轮播图2
    DOM
    js五星好评
  • 原文地址:https://www.cnblogs.com/walkinhalo/p/10799768.html
Copyright © 2011-2022 走看看