精华内容
下载资源
问答
  • 数据源的应用很大程度上是为了满足多租户业务场景,多租户是一种软件架构技术,是实现如何在多用户环境下共用相同的系统或程序组件,并且可确保各用户间数据的隔离性,多租户的重点就是同一套程序下实现多用户数据...

    多数据源应用场景

    多数据源的应用很大程度上是为了满足多租户业务场景,多租户是一种软件架构技术,是实现如何在多用户环境下共用相同的系统或程序组件,并且可确保各用户间数据的隔离性,多租户的重点就是同一套程序下实现多用户数据的隔离。因此,多数据源就在这种场景下应运而生。

    目前是实现多数据源的技术方案大致可以分为三类

    1)每个用户都对应一个独立的数据库

    2)每个用户共享数据库,同时每个用户对应一个独立的schema

    3)通过在表中增加一个字段标识来区别数据所属的用户

    上面三种数据隔离的实现方式都会有自己的业务场景价值,隔离级别高,安全性好,就会增加数据库的部署数量和维护成本,相对,隔离级别低,安全性低,就会增加代码的开发复杂度。

    spring集成多数据源

    1)数据源集成

    上面说明了三种多租户的实现方式,这里仅介绍前两种在spring中一个通用的实现方式--AbstractRoutingDataSource

    AbstractRoutingDataSource的内部维护了一个名为targetDataSources的Map和一个默认数据源defaultTargetDataSource,这两个参数可以在服务启动成功后进行设置,根据系统涉及到的数据源种类数量,对targetDataSources进行初始设置,其中key对应数据源的名称,value对应一个DataSource的实现;用户在切换数据源时根据数据源名称在 targetDataSources找DataSource时,如果没有相应的数据源,则会使用默认数据源defaultTargetDataSource。

    例子说明:

    本人工作中因业务需求,需要在PG数据库中基于多个schema做数据隔离实现多租户,数据库结构如下:

     

    上面截图中有四个不同的schema,在服务启动后将这四个schema加载到targetDataSources,并将public设置为defaultTargetDataSource,本人对项目代码做了删减,具体参考如下:

    @Autowired
    private AbstractRoutingDataSource multiRouteDataSource;
    
    public Map<String, DruidDataSource> DATA_SOURCE_MAP = new HashMap<>();
    	
    	@EventListener
        public void refreshDataSources(ApplicationReadyEvent event) {
            Runnable loadResource = new Runnable() {
                @Override
                public void run() {
                    loadDataSources();
                }
            };
            Thread thread = new Thread(loadResource);
            thread.start();
            log.info(LogFormatter.toMsg("load resource"));
        }
        public void loadDataSources(){
            DataSourceContext.setDataSource("public");
    		//获取所有的schema,本项目是将所有schema放在public的特定表中。可以根据实际情况决定去存储schema的信息
            List<String> schemaList = bussinessDao.getAllSchema(); 
            DataSourceContext.clearDataSource();
            //过滤掉一些PG自带的schema
            Iterator<String> iterator = schemaList.iterator();
            while(iterator.hasNext()){
                String schema = iterator.next();
                if(schema.startsWith("pg_")){
                    iterator.remove();
                }
            }
            if (schemaList != null && schemaList.size() > 0) {
                Map<Object, Object> targetDataSources = new HashMap<>();
                for (String schema : schemaList) {
                    log.info(LogFormatter.toMsg("multiDataSource init","schema"), schema);
                    if(ObjectUtils.isNotEmpty(DATA_SOURCE_MAP.get(schema))){
                        targetDataSources.put(schema, DATA_SOURCE_MAP.get(schema));
                    }else{
                        //根据schema初始化数据源,具体实现省略
                        DruidDataSource dataSource = initDataSource(schema) 
                        DATA_SOURCE_MAP.put(schema, dataSource);
                        targetDataSources.put(schema, dataSource);
                    }
                }
    
                if(ObjectUtils.isEmpty(DATA_SOURCE_MAP.get("public"))){
                    DruidDataSource dataSource = initDataSource("public");
                    DATA_SOURCE_MAP.put("public", dataSource);
                }
                //加入默认dataSource
                targetDataSources.put("public", DATA_SOURCE_MAP.get("public")); 
                //设置targetDataSources
                multiRouteDataSource.setTargetDataSources(targetDataSources); 
                //设置默认数据源
                Object dataSource = DATA_SOURCE_MAP.get("public")
                multiRouteDataSource.setDefaultTargetDataSource(dataSource); 
            }
            multiRouteDataSource.afterPropertiesSet();
        }
    }

    2)数据源切换

    多数据源的目的是实现数据隔离,所以针对不同用户就要切换对应的数据源,这样才能避免数据污染,实现业务数据的正确。AbstractRoutingDataSource中利用模板模式,通过自身的抽象方法determineCurrentLookupKey()决定当前的数据源,AbstractRoutingDataSource获取数据源的源码如下:

    protected DataSource determineTargetDataSource() {
       Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
       Object lookupKey = determineCurrentLookupKey();
       DataSource dataSource = this.resolvedDataSources.get(lookupKey);
       if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
          dataSource = this.resolvedDefaultDataSource;
       }
       if (dataSource == null) {
          throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
       }
       return dataSource;
    }

    用户数据源的切换逻辑可以通过实现determineCurrentLookupKey()方法进行满足,一般情况就是获取当前线程上下文中的schema名称,具体实现思路:用户可以通过ThreadLocal设置当前线程的schema名称来决定使用哪个数据源,然后在具体实现的determineCurrentLookupKey()方法中通过ThreadLocal的get()方法获取schema名称,这样determineTargetDataSource方法就会根据这个schema从targetDataSources获取对应的数据源。

    事务中切换数据源问题

    很多人在使用多数据源时,都会遇到事务中不能切换数据源的情况,这个时候我们就需要对源码有所了解,接下来从源码的角度来说明为什么在事务中不能切换数据源:我们知道spring是通过DataSourceTransactionManager对事务进行管理的,当我们在执行事务方法时,会通过AOP机制先执行DataSourceTransactionManager的doBegin()方法:

    其中doBegin方法中的Connection newCon = obtainDataSource().getConnection()是用来获取具体的数据库connection,随着断点对obtainDataSource().getConnection()进行跟进,发现它最终会调用AbstractRoutingDataSource的getConnection()方法:

    看到这个方法,我们应该就会很熟悉了,这个方法中的determineTargetDataSource()上面已经说过,它是根据determineCurrentLookupKey()决定当前线程使用哪个数据源。 

    通过上面的分析我们可以知道事务方法开始前就会根据当前数据源获取一个数据库连接connection,这个connection在当前线程的上下文中进行缓存,事务方法结束前都是可复用的,不然如何保证数据的事务特性,所以我们在事务的过程中是不需要更新connection的,不更新connection也就不会执行AbstractRoutingDataSource的getConnection()方法,从而不会更新数据源。这里需要说明的是:spring事务不仅仅针对加了@Transactional注解的方法,对数据库进行增删改的时候,spring也是通过DataSourceTransactionManager进行管理的。

    上图是mybatis执行器,比如SimpleExecutor,创建Statement获取connection时的一段源码分析,通过分析可以发现,执行器在获取connection时,会根据当前线程上下文是否有connection,来决定进行connection复用,还是通过fetchConnection()获取connection,其中fetchConnection()是根据AbstractRoutingDataSource的getConnection()方法获取connection。因此,我们要想切换数据源只能在事务开启前进行切换,我们知道事务也是基于AOP实现的,这样我们可以把切换数据源的操作也放在AOP中,然后设置该AOP的order高于事务即可,比如,在该AOP中添加@Order(-1)注解

    展开全文
  • 对于同一个方法使用不同数据源时配合spring的事务为何会切换数据源失败?那如何解决?如果解决了那会不会有分布式事务的问题(解决的方案可能是分不同事务去解决的,那不同事务就存在事务一致性(分布式事务的问题...

    对于springboot使用多数据源,有一个点就是对于数据源的切换是值得分析的。

    数据源切换方式有几种?

    对于同一个方法中使用不同数据源时配合spring的事务为何会切换数据源失败?那如何解决?如果解决了那会不会有分布式事务的问题(解决的方案可能是分不同事务去解决的,那不同事务就存在事务一致性(分布式事务的问题))

    一、实现多数据源的方式

    实现多数据源的方式主要有三种

    1、不同包(实际不常用)

    2、AOP(基于 Spring AbstractRoutingDataSource 做拓展)

    3、数据库中间件(比较完美解决)

    其中第二种和第三种较多人用

    这里对于第二种情况进行分析,由于在第二种情况下结合spring事务会产生数据源切换不成功情况,此时则使用开源 baomidou 的一个项目dynamic-datasource-spring-boot-starter来实现多数据源的功能。

    二、baomidou 多数据源(dynamic-datasource-spring-boot-starter

    关于 dynamic-datasource-spring-boot-starter 的介绍,胖友自己看 官方文档 。😈 它和 MyBatis-Plus 都是开发者 baomidou 提供的。

    1、pom文件:

     <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.3.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!-- 实现对数据库连接池的自动化配置 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <dependency> <!-- 本示例,我们使用 MySQL -->
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.48</version>
            </dependency>
    
            <!-- 实现对 MyBatis 的自动化配置 -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.1</version>
            </dependency>
    
            <!-- 实现对 dynamic-datasource 的自动化配置 -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
                <version>2.5.7</version>
            </dependency>
            <!-- 不造为啥 dynamic-datasource-spring-boot-starter 会依赖这个 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-actuator</artifactId>
            </dependency>
    
            <!-- 方便等会写单元测试 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
        </dependencies>

    yml (可以自己加上druid等连接池,这里为了简化容易看就不加了)

    spring:
      datasource:
        # dynamic-datasource-spring-boot-starter 动态数据源的配置内容
        dynamic:
          primary: users # 设置默认的数据源或者数据源组,默认值即为 master
          datasource:
            # 订单 orders 数据源配置
            orders:
              url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
              driver-class-name: com.mysql.jdbc.Driver
              username: root
              password: 123456
            # 用户 users 数据源配置
            users:
              url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
              driver-class-name: com.mysql.jdbc.Driver
              username: root
              password: 123456
    
    # mybatis 配置内容
    mybatis:
      #config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路径
      mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
      type-aliases-package: com.datasource.entity # 配置数据库实体包路径
      configuration:
        map-underscore-to-camel-case: true  #开启驼峰
    server:
      port: 9001

    启动类:

    @SpringBootApplication
    @MapperScan(basePackages = "com.datasource.mapper")//扫描mapper接口
    @EnableAspectJAutoProxy(exposeProxy = true)//配置 Spring AOP 能将当前代理对象设置到 AopContext 中,后面我们需要用AopContext拿到aop的代理对象
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class,args);
        }
    }

    枚举类:
     

    //枚举类
    public class DBConstants {
    
        /**
         * 数据源分组 - 订单库
         */
        public static final String DATASOURCE_ORDERS = "orders";
    
        /**
         * 数据源分组 - 用户库
         */
        public static final String DATASOURCE_USERS = "users";
    
    }

    新建order库和order表

    新建user库和表

    实体类:

    order

    @Data
    public class Order implements Serializable {
    
        private int id;
        private int userId;
    }
    

    user

    @Data
    public class User implements Serializable {
    
    
        private int id;
        private String username;
        private String password;
        private Date createTime;
    
    }
    

    mapper接口:

    //order的mapper接口
    @Repository
    @DS(DBConstants.DATASOURCE_ORDERS)
    public interface OrderMapper {
    
        Order selectById(@Param("id") Integer id);
    
    }
    
    //user的mapper接口
    @Repository
    @DS(DBConstants.DATASOURCE_USERS)//dynamic-datasource-spring-boot-starter,数据源名
    public interface UserMapper {
    
        User selectById(@Param("id") Integer id);
    
    }

    xml:
     

    //order的xml文件
    <mapper namespace="com.datasource.mapper.OrderMapper">
    
        <sql id="FIELDS">
            id, user_id
        </sql>
    
        <select id="selectById" parameterType="integer" resultType="com.datasource.entity.Order">
            SELECT
            <include refid="FIELDS" />
            FROM orders
            WHERE id = #{id}
        </select>
    
    </mapper>
    
    //user的xml文件
    <mapper namespace="com.datasource.mapper.UserMapper">
    
        <sql id="FIELDS">
            id, username,password,create_time
        </sql>
    
        <select id="selectById" parameterType="integer" resultType="com.datasource.entity.User">
            SELECT
            <include refid="FIELDS" />
            FROM users
            WHERE id = #{id}
        </select>
    
    </mapper>

    service层(这里设置了5个场景,等下分别来分析各个场景的问题)

    @Service
    public class OrderService {
    
        @Autowired
        private OrderMapper orderMapper;
        @Autowired
        private UserMapper userMapper;
    
    
        //获取到aop的代理对象(如果用this调用此时拿到的不是aop的代理对象)
        private OrderService self() {
            return (OrderService) AopContext.currentProxy();
        }
    
    //-------------------------场景1----------------------------
    //未开启事务,则可以自己使用自己的数据源,并不会报异常
        public void method01() {
            // 查询订单
            Order order = orderMapper.selectById(1);
            System.out.println(order);
            // 查询用户
            User user = userMapper.selectById(1);
            System.out.println(user);
        }
    
    //-------------------------场景2----------------------------
    
        //此时开启事务(经过aop代理),会抛出找不到表的情况,因为此时事务开启就会默认拿到默认的数据源user,
        // 而第一次查询是要使用order数据源的,此时因为spring事务则无法从默认的user切换到order数据源上
        //这里就是结合spring事务出现的数据源切换不成功的情况(具体原因后面分析)
        @Transactional
        public void method02() {
            // 查询订单
            Order order = orderMapper.selectById(1);
            System.out.println(order);
            // 查询用户
            User user = userMapper.selectById(1);
            System.out.println(user);
        }
    
    //-------------------------场景3----------------------------
    
        public void method03() {
            // 查询订单
            self().method031();
            // 查询用户
            self().method032();
        }
    
        @Transactional // 报错,因为此时获取的是 primary 对应的 DataSource ,即 users 。
        public void method031() {
            Order order = orderMapper.selectById(1);
            System.out.println(order);
        }
    
        @Transactional
        public void method032() {
            User user = userMapper.selectById(1);
            System.out.println(user);
        }
    
    //-------------------------场景4----------------------------
    
        //未报异常,正常结束(两个方法各自执行各自的事务,并且根据@DS获取到各自的数据源)
        public void method04() {
            // 查询订单
            self().method041();
            // 查询用户
            self().method042();
        }
    
        @Transactional
        @DS(DBConstants.DATASOURCE_ORDERS)
        public void method041() {
            Order order = orderMapper.selectById(1);
            System.out.println(order);
        }
    
        @Transactional
        @DS(DBConstants.DATASOURCE_USERS)
        public void method042() {
            User user = userMapper.selectById(1);
            System.out.println(user);
        }
    
    //-------------------------场景5----------------------------
        //正常执行,05方法中调用052方法,在一个事务中调用另一个事务方法,
        // 另一个事务方法采用,重新建立一个事务的方法并使用自己的数据源
        // 此时在05方法中则会出现两个事务,这时就涉及到分布式事务
    
        @Transactional
        @DS(DBConstants.DATASOURCE_ORDERS)//声明自己要用到的数据源,这样就不会去拿默认数据源了
        public void method05() {
            // 查询订单
            Order order = orderMapper.selectById(1);
            System.out.println(order);
            // 查询用户
            self().method052();
        }
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        @DS(DBConstants.DATASOURCE_USERS)
        public void method052() {
            User user = userMapper.selectById(1);
            System.out.println(user);
        }
    
     
    
    }

    此时用controller的接口来测试各个场景

    @RestController
    public class DatasourceController {
    
        @Autowired
        OrderService orderService;
    
        //场景1
        @GetMapping("/method1")
        public void getMethod1(){
    
            orderService.method01();
        }
    
        //场景2
        @GetMapping("/method2")
        public void getMethod2(){
    
            orderService.method02();
        }
        //场景3
        @GetMapping("/method3")
        public void getMethod3(){
    
            orderService.method03();
        }
        //场景4
        @GetMapping("/method4")
        public void getMethod4(){
    
            orderService.method04();
        }
        //场景5
        @GetMapping("/method5")
        public void getMethod5(){
    
            orderService.method05();
        }
    
    
    
    }
    

    下面针对各个场景进行分析

    1、场景1

    此时场景1未使用spring的事务,即没开启事务,访问场景1接口http://localhost:9001/method1

    此时运行正常,控制台打印出正常信息

    原因:

    • 方法未使用 @Transactional 注解,不会开启事务。
    • 对于 OrderMapper 和 UserMapper 的查询操作,分别使用其接口上的 @DS 注解,找到对应的数据源,执行操作。
    • 这样一看,在未开启事务的情况下,我们已经能够自由的使用多数据源进行切换。
     public void method01() {
            // 查询订单
            Order order = orderMapper.selectById(1);
            System.out.println(order);
            // 查询用户
            User user = userMapper.selectById(1);
            System.out.println(user);
        }

    2、场景2

    
        @Transactional
        public void method02() {
            // 查询订单
            Order order = orderMapper.selectById(1);
            System.out.println(order);
            // 查询用户
            User user = userMapper.selectById(1);
            System.out.println(user);
        }

    此时报错:Table 'test_users.orders' doesn't exist  (即在tset_users数据库中找不到orders表)

    为什么?我们不是在mapper接口各自的使用的各自的数据源?

    注意,此时的错误发生在orderMapper查询该行代码上,而此时从错误中我们可以看出他是在user库中找order表?

    那不就是说我们在执行orderMapper接口查询时拿到的是user的数据源信息(所以才会去user库)

    那么这是为什么呢?我们不是在 OrderMapper 上,声明使用 orders 数据源了么?结果为什么会使用 users 数据库,路由到 test_users 库上呢。

    原因如下:

    • 这里,就和 Spring 事务的实现机制有关系。因为方法添加了 @Transactional 注解,Spring 事务就会生效。此时,Spring TransactionInterceptor 会通过 AOP 拦截该方法,创建事务。而创建事务,势必就会获得数据源。那么,TransactionInterceptor 会使用 Spring DataSourceTransactionManager 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。
    • 而事务信息,就包括事务对应的 Connection 连接。那也就意味着,还没走到 OrderMapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 OrderMapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 OrderMapper 添加 @DS 注解所对应的 DataSource 所对应的 Connection 。
    • OK ,那么我们现在可以把问题聚焦到 DataSourceTransactionManager 是怎么获取 DataSource 从而获得 Connection 的了。对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用 dynamic-datasource-spring-boot-starter 时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。
    • 而 DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了 ordersusers 两个数据源,并且默认使用 users 数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于 @DS 获得数据源名,从而获得对应的 DataSource ,结果因为我们在 Service 方法上,并没有添加 @DS 注解,所以它只好返回默认数据源,也就是 users 。故此,就发生了 Table 'test_users.orders' doesn't exist 的异常。

    也就是说此时要切换数据源,需要在service层(即 @Transactional的方法上设置是哪个数据源,这样DynamicRoutingDataSource才会去拿到相应的数据源)设置使用哪个数据源,如果不设置此时DynamicRoutingDataSource会去拿到我们默认的数据源(即user),而后面的场景4就是根据这样实现的。

    (那么这里提出一个疑问:mapper接口上设置的数据源不就不起作用?那是为什么呢?这个问题需要我再去学学然后再回来解释)(亲测在场景4中使用了在service设置数据源后删掉mapper接口的数据源设置后没有影响结果)

    3、场景3

    public void method03() {
        // 查询订单
        self().method031();
        // 查询用户
        self().method032();
    }
    
    @Transactional // 报错,因为此时获取的是 primary 对应的 DataSource ,即 users 。
    public void method031() {
        OrderDO order = orderMapper.selectById(1);
        System.out.println(order);
    }
    
    @Transactional
    public void method032() {
        UserDO user = userMapper.selectById(1);
        System.out.println(user);
    }

    抛出异常:Table 'test_users.orders' doesn't exist

    和场景2等价的。

    其中self()是获取到aop的代理对象,如果此时,我们将 #self() 代码替换成 this 之后,诶,结果就正常执行。这又是为什么呢?其实,这样调整后,因为 this 不是代理对象,所以 #method031() 和 #method032() 方法上的 @Transactional 直接没有作用,Spring 事务根本没有生效。所以,最终结果和场景一是等价的。

    4、场景4

    public void method04() {
        // 查询订单
        self().method041();
        // 查询用户
        self().method042();
    }
    
    @Transactional
    @DS(DBConstants.DATASOURCE_ORDERS)
    public void method041() {
        OrderDO order = orderMapper.selectById(1);
        System.out.println(order);
    }
    
    @Transactional
    @DS(DBConstants.DATASOURCE_USERS)
    public void method042() {
        UserDO user = userMapper.selectById(1);
        System.out.println(user);
    }

    正常执行,原因:

    • 和 @method03() 方法,差异在于,#method041() 和 #method042() 方法上,添加 @DS 注解,声明对应使用的 DataSource 。
    • 执行方法,正常结束,未抛出异常。是不是觉得有点奇怪?
    • 在执行 #method041() 方法前,因为有 @Transactional 注解,所以 Spring 事务机制触发。DynamicRoutingDataSource 根据 @DS 注解,获得对应的 orders 的 DataSource ,从而获得 Connection 。所以后续 OrderMapper 执行查询操作时,即使使用的是线程绑定的 Connection ,也可能不会报错。😈 嘿嘿,实际上,此时 OrderMapper 上的 @DS 注解,也没有作用。
    • 对于 #method042() ,也是同理。但是,我们上面不是提了 Connection 会绑定在当前线程么?那么,在 #method042() 方法中,应该使用的是 #method041() 的 orders 对应的 Connection 呀。在 Spring 事务机制中,在一个事务执行完成后,会将事务信息和当前线程解绑。所以,在执行 #method042() 方法前,又可以执行一轮事务的逻辑。
    • 【重要】总的来说,对于声明了 @Transactional 的 Service 方法上,也同时通过 @DS 声明对应的数据源。

           这时就解决了场景2、3的数据源切换不成功的问题了,即在各个@Transactional设置不同的数据源,但这里要明白,设置不同的数据源,此时场景4的method4方法是启动了两个事务,则此时就会有分布式事务问题(即可能事务1执行完成,事务2回滚了,而method4实际要达到的目的是事务1、2要么都完成,要么都回滚,所以这里的分布式事务需要进一步解决)

    5、场景5

    @Transactional
    @DS(DBConstants.DATASOURCE_ORDERS)
    public void method05() {
        // 查询订单
        OrderDO order = orderMapper.selectById(1);
        System.out.println(order);
        // 查询用户
        self().method052();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @DS(DBConstants.DATASOURCE_USERS)
    public void method052() {
        UserDO user = userMapper.selectById(1);
        System.out.println(user);
    }

    在一个事务中重开一个事务,这时也解决了事务中切换不成功的情况

    执行正常,原因:

    • 和 @method04() 方法,差异在于,我们直接在 #method05() 方法中,此时处于一个事务中,直接调用了 #method052() 方法。
    • 执行方法,正常结束,未抛出异常。是不是觉得有点奇怪?
    • 我们仔细看看 #method052() 方法,我们添加的 @Transactionl 注解,使用的事务传播级别是 Propagation.REQUIRES_NEW 。此时,在执行 #method052() 方法之前,TransactionInterceptor 会将原事务挂起,暂时性的将原事务信息和当前线程解绑。
      • 所以,在执行 #method052() 方法前,又可以执行一轮事务的逻辑。
      • 之后,在执行 #method052() 方法完成后,会将原事务恢复,重新将原事务信息和当前线程绑定。

    注意:这里由于开启了两个事务,也会存在分布式事务问题。

    而这个场景则说明如何在一个事务方法中切换数据源

    上面是对数据源源切换使用做了介绍,而多数据源也涉及了读写分离、分库分表的实现,虽然上面这种方案也可以实现,但是每次都要去配置一个数据源和其他配置信息1,这会非常的麻烦,那么下面会介绍一款开源的数据库中间件来解决这些事情

    我在看见网上还有其他的解决方案:大概是不使用spring的声明式事务,自己提交回滚事务,从而达到切换数据源成功(即编程式事务)https://www.cnblogs.com/cq-yangzhou/p/10945779.html

    https://blog.csdn.net/u010928589/article/details/91348761

    https://blog.csdn.net/qq1010830256/article/details/106652663

    (这个方法也值得我分析实现)

    sharedingsphere

    上面对于基于 Spring AbstractRoutingDataSource 做拓展(aop)的方案结合开源包进行了分析,虽然上面可以进行数据源切换,但是其解决方法中开启了多个事务,此时则会面临分布式事务的问题,现在对于分布式事务的解决方案,主要有下面几个方案:
    刚性事务/低并发:2/3pc

    柔性事务/长事务/高并发:tcc、saga、mq等

    那么我们选择什么呢?

    此时我们就需要选择一个阿帕奇的开源项目sharedingsphere,其中包含shareding jdbc(client模式),sharding proxy(proxy代理模式)、和正在开发的shareding sidecar。

    我们可以使用其中的shareding jdbc或者sharding proxy,这里我们主要选择前者,其一少了一层代理层性能更高,但是配置可能会麻烦一点(现在比较多的大厂还是在用client模式的)。而且对于分布式事务,sharedingsphere也支持刚性柔性事务(seata等框架),可以完美的解决多数据源、分库分表、读写分离、分布式事务的解决方案。

    其实还有一款比较多人使用的数据库中间件mycat(不太适合高并发)(client模式)

    下面有关一个分库分表技术说明文章:https://www.bilibili.com/read/cv7536093?spm_id_from=333.788.b_636f6d6d656e74.23

    对于sharedingsphere我不打算在这篇文章讲,后面会写关于怎么使用sharedingsphere来实现多数据源切换、分库分表、读写分离、分布式事务问题的实现。

    本文主要参考这位大哥的文章:http://www.iocoder.cn/Spring-Boot/dynamic-datasource/#

    展开全文
  • 我现在的思路是在我的application配置两个数据库 利用aop自动切换数据库 ...我是为了实现日志和生产的数据源分离,而我在保存日志的时候都是直接在事务层调我日志的service,这样可以实现吗
  • 多数据源是否用到连接池? 在spring获取连接处我们看到了这个是如何获取连接的 ...看这个问题前,先考虑一个问题:我们需要在事务中切换数据源,会生效吗? 答案是不可以,因为在事务开启后,会复用d...

    多数据源是否用到连接池?

    在spring获取连接处我们看到了这个是如何获取连接的

    上面④数据源就是我们在多数据源配置的信息

     从上面可以看出关联关系:dataSource -> connctionpool. 也就是说,连接池是放在数据源中的

    多数据源事务为何失效?

    看这个问题前,先考虑一个问题:我们需要在事务中切换数据源,会生效吗?

    答案是不可以,因为在事务开启后,会复用 dataSource的Connection。复用dataSource 的Connection 会导致无法切换数据源

     

    spring中数据源调用方式如下:

     参考:Transaction事务注解和DynamicDataSource动态数据源切换问题解决

    一般代码调用链:
    Repository@Annotation(AOP)-->DefaultSqlSession-->SimpleExecutor-->BaseExecutor.getConnection()-->SpringManagedTransaction.getConnection()--->连接为空-->AbstractRoutingDataSource.getConnection()-->拿到beforeAOP中注入的datasource的key, 所以每次都会动态切换数据源
    事务代码调用链:
    service注解上@transactional-->TransactionInterceptor.interpter()-->TransactionAspectSupport.createTransactionIfNecessary()-->AbstractPlatformTransactionManager.getTransaction()-->DataSourceTransactionManager.doBegin()-->AbstractRoutingDataSource.determineTargetDataSource()[lookupKey==null去拿默认的Datasource, 不为空则使用获取到的连接]-->DataSourceTransactionManager.setTransactional()[将连接设置到TransactionUtils的threadLocal中]--->Repository@Annotation-->执行一般调用链, 问题在于SpringManagedTransaction.getConnection()-->openConnection()-->DataSourceUtils.getConnection()-->TransactionSynchronizationManager.getResource(dataSource)不为空[从TransactionUtils的threadLocal中获取数据源], 所以不会再去调用DynamicDataSource去获取数据源

     这个是我们设置的threadlocal中获取的datasource名称,然后根据名称获取(开始配置时,以key,value放入map中存储了)

     总结一下:事务开启后,datasource 的连接会复用,但是如果不开启事务,会一直走fetchConnection更新连接,每次都去获取我们配置的(一般设置在threadLocal变量中)。

    接着我们解释一下为什么多数据源事务失效。

    多数据源对应的是不同的datasource连接,回滚是在spring aop方法执行后捕获异常,然后获取threadLocal中同一个连接回滚的。

    展开全文
  • 多数据源是否用到连接池? 在spring获取连接处我们看到了这个是如何获取连接的 上面④数据源就是我们在多数据...看这个问题前,先考虑一个问题:我们需要在事务中切换数据源,会生效吗? 答案是不可以,因为在事...

    多数据源是否用到连接池?

    在spring获取连接处我们看到了这个是如何获取连接的

     

     

    上面④数据源就是我们在多数据源配置的信息

     

     从上面可以看出关联关系:dataSource -> connctionpool. 也就是说,连接池是放在数据源中的。

     

     

    多数据源事务为何失效?

    看这个问题前,先考虑一个问题:我们需要在事务中切换数据源,会生效吗?

    答案是不可以,因为在事务开启后,会复用 dataSource。复用dataSource 会导致无法切换数据源;

     

    spring中数据源调用方式如下:

     

    一般代码调用链:
    Repository@Annotation(AOP)-->DefaultSqlSession-->SimpleExecutor-->BaseExecutor.getConnection()-->SpringManagedTransaction.getConnection()--->连接为空-->AbstractRoutingDataSource.getConnection()-->拿到beforeAOP中注入的datasource的key, 所以每次都会动态切换数据源
     
    事务代码调用链:
    service注解上@transactional-->TransactionInterceptor.interpter()-->TransactionAspectSupport.createTransactionIfNecessary()-->AbstractPlatformTransactionManager.getTransaction()-->DataSourceTransactionManager.doBegin()-->AbstractRoutingDataSource.determineTargetDataSource()[lookupKey==null去拿默认的Datasource, 不为空则使用获取到的连接]-->DataSourceTransactionManager.setTransactional()[将连接设置到TransactionUtils的threadLocal中]--->Repository@Annotation-->执行一般调用链, 问题在于SpringManagedTransaction.getConnection()-->openConnection()-->DataSourceUtils.getConnection()-->TransactionSynchronizationManager.getResource(dataSource)不为空[从TransactionUtils的threadLocal中获取数据源], 所以不会再去调用DynamicDataSource去获取数据源
     
     
     

     

     

     

     

     

     

     

     

    展开全文
  • 最近项目用到多数据源 ,数据源切换的时候 现在是在action中切换的 放到了session 那么问题就来了 session失效了 数据源自动切换成默认的,我想问能否在到dao中切换 在dao中切换的话 事务如何控制呢
  •  spring的事务管理,是基于数据源的,所以如果要实现动态数据源切换,而且在同一个数据源中保证事务是起作用的话,就需要注意二者的顺序问题,即:在事务起作用之前就要把数据源切换回来。  举一个例子:web开发...
  • Spring分布式动态数据源事务

    万次阅读 热门讨论 2016-08-05 22:52:11
    本篇是一个真实案例,来源于与一位技术达人一起共同探讨,解决一个分布式事务中如何动态管理,切换数据库,适用于大型互联网,及分库管理系统。网上虽有很多相似问题,但却无可靠,有效之解决方案。
  • 通过springboot操作mysql数据库,但是在实际业务场景中,数据量迅速增长,一个库一个表已经满足不了我们的需求的时候,我们就会考虑分库分表的操作,在springboot中如何实现多数据源,动态数据源切换,读写分离等...
  • Spring 多数据源

    2020-04-21 22:35:20
    本文会介绍spring多数据源的典型场景,如何优雅的实现多数据源,并结合spring、mybatis源码进行分析,为什么在事务中,不能切换数据源。最后,还会提供一个多数据源的完整源码案例。 1 多数据源的典型使用场景 在...
  • 剖析Spring多数据源

    2020-06-09 11:29:11
    本文会介绍spring多数据源的典型场景,如何优雅的实现多数据源,并结合spring、mybatis源码进行分析,为什么在事务中,不能切换数据源。最后,还会提供一个多数据源的完整源码案例。 1 多数据源的典型使用场景 ...
  • 14.玩转Spring Boot 多数据源

    千次阅读 2016-12-28 11:33:14
    玩转Spring Boot 多数据源 在项目有的时候需要用到...具体不做叙述,接下来说如何实现多数据源并且使用AOP来切换。 本例代码使用Mybatis具体请看:10.玩转Spring Boot 集成Mybatis,11.玩转Spring Boot 集成Druid
  • 项目开发经常会遇到读写分离等多数据源配置的需求,在Java项目可以通过Spring AOP来实现多数据源切换。 一、Spring事务开启流程  Spring通常通过@Transactional来声明使用事务,我们先来研究一下Spring是...
  • 实例226 如何在程序创建Access数据库ODBC数据源 实例227 如何在程序运行时创建数据库BDE别名 实例228 如何在安装程序设置BDE引擎 实例229 如何消除MS-SQL Server数据库连接的登录框 实例230 如何连接MS-SQL ...
  • 实例226 如何在程序创建Access数据库ODBC数据源 实例227 如何在程序运行时创建数据库BDE别名 实例228 如何在安装程序设置BDE引擎 实例229 如何消除MS-SQL Server数据库连接的登录框 实例230 如何连接MS-SQL ...
  • 实例226 如何在程序创建Access数据库ODBC数据源 实例227 如何在程序运行时创建数据库BDE别名 实例228 如何在安装程序设置BDE引擎 实例229 如何消除MS-SQL Server数据库连接的登录框 实例230 如何连接MS-SQL ...
  • Java生成密钥的实例 1个目标文件 摘要:Java源码,算法相关,密钥 Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的...保存、如何从文件得到公钥编码的字节数组、如何从字节数组解码公钥...
  • 精通qt4编程(代码)

    热门讨论 2010-03-17 19:10:40
    \ 第11章 事件机制 李立夏介绍了Qt的事件处理模型,详细介绍了在Qt程序设计处理事件的五种方法,并讨论了如何利用Qt事件机制加快用户界面响应速度。 283 \ 第12章 数据库 李立夏介绍了Qt的数据库处理,重点介绍了...
  • 本书延续了Ivor Horton讲解编程语言的独特方法,从中读者可以学习Visual C++ 2005的基础知识,并全面掌握在MFC和Windows Forms访问数据源的技术。此外,本书各章后面的习题将有助于读者温故而知新,并尽快成为C++...
  • 精通Qt4编程(第二版)代码

    热门讨论 2014-01-19 13:07:18
    \12.2.2 事务操作 304 \12.2.3 使用SQL模型类 304 \12.2.4 数据表示 308 \12.3 Qt数据库应用 310 \12.3.1 使用嵌入式数据库 310 \12.3.2 使用Oracle数据库 313 \12.4 小结 325 \第13章 Qt的模板库和工具类 ...
  • ABAP中文幫助文檔

    2018-10-27 19:07:09
    另外,本节也描述如何显示或更改现有程序以及如何从编辑器启动程序。 此处描述的创建新ABAP/4程序的过程适用于报表和短培训程序。在开始编写报表程序之前,用户也许想先创建报表和短培训程序以熟悉ABAP/4语法。要...
  •  第3~9行定义了一个数据源,其实现类是apache的BasicDataSource,第11~25行定义了Hibernate的会话工厂,会话工厂类用Spring提供的LocalSessionFactoryBean维护,它注入了数据源和资源映射文件,此外还通过一些键值...
  • MyEclipse 6 Java 开发中文教程(完整版)

    热门讨论 2012-11-08 15:32:34
    10.5.2.3 用Spring 1.2 的事务代理类解决事务提交问题 240 10.5.2.4 用Spring 2.0 的aop和tx声明式配置解决事务提交问题 242 10.5.2.5 用Spring 2.0 的@Transactional标注解决事务提交问题(最佳方案) . 246 10.5....
  • 第一部分 界面设计

    2013-10-22 23:10:34
    实例226 如何在程序创建Access数据库ODBC数据源 实例227 如何在程序运行时创建数据库BDE别名 实例228 如何在安装程序设置BDE引擎 实例229 如何消除MS-SQL Server数据库连接的登录框 实例230 如何连接MS-SQL ...
  • 第一章 安装配置开发环境 18 1.1系统需求 18 1.2 JDK 的下载,安装和配置(可...10.7.2 MyEclipse生成的Spring+Hibernate无法保存数据问题的解决方法2 - 用 CGLIB 来实现事务管理 258 10.7.3 Spring相关的参考资料 261
  • Visual C++编程技巧精选500例.pdf

    热门讨论 2012-09-01 15:01:50
    181 如何在两个执行程序间进行数据通信? 182 如何使用工作线程? 183 如何正常终止线程? 184 如何异常终止线程? 185 如何获取线程退出码? 186 如何使用线程优先级? 187 如何使用用户界面线程? 188 如何实现多线程多...
  • 说明: 如果值为TRUE, 即使长度比目标长度 (SQL92 兼容) 更长, 也允许分配数据。 值范围: TRUE | FALSE 默认值: FALSE serializable: 说明: 确定查询是否获取表级的读取锁, 以防止在包含该查询的事务处理被提交...
  • java源码包---java 源码 大量 实例

    千次下载 热门讨论 2013-04-18 23:15:26
     Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节...保存、如何从文件得到公钥编码的字节数组、如何从字节数组解码公钥...
  • 面试官:如何以最高的效率从MySQL随机查询一条记录? 面试官:讲讲Redis的虚拟内存? 云原生系列 三万字无坑搭建基于Docker+K8S+GitLab/SVN+Jenkins+Harbor持续集成交付环境!! 冰河教你一次性成功安装K8S集群...

空空如也

空空如也

1 2 3 4 5
收藏数 100
精华内容 40
关键字:

事务中如何切换数据源