前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据库读写分离与事务纠缠的那点坑

数据库读写分离与事务纠缠的那点坑

作者头像
AI乔治
修改2019-08-01 10:30:55
1.3K0
修改2019-08-01 10:30:55
举报
文章被收录于专栏:JAVA烂猪皮

1. 在读写分离时会不会造成事务主从切换错误

一个线程在Serivcie时Select时选择的是从库,DynamicDataSourceHolder中ThreadLocal对应线程存储的是slave,然后调用Manager时进入事务,事务使用默认的transacatinManager关联的dataSource,而此时会不会获取到的是slave?

2. 事务隔离级别和传播特性会不会影响数据连接池死锁

一个线程在Service层Select数据会从数据库获取一个Connection,通常来讲,后续DB的操作在同一线线程会复用这个DB Connection,但是从Service进入Manager的事务后,Get Seq获取全局唯一标识,所以Get Seq一般都会开启新的事物从DB Pool里重新获取一个新连接进行操作,但是问题是如果两个事务关联的datasource是同一个,即DB Pool是同一个,那么如果DB Pool已经为空,是否会造成死锁?

为了减轻数据库的压力,一般会进行数据库的读写分离,实现方法一是通过分析sql语句是insert/select/update/delete中的哪一种,从而对应选择主从,二是通过拦截方法名称的方式来决定主从的,如:save()、insert() 形式的方法使用master库,select()开头的使用slave库。

通常在方法上标上自定义标签来选择主从。

代码语言:javascript
复制
@DataSource("slave")
int queryForCount(OrderQueryCondition queryCondition);

或者通过拦截器动态选择主从。

代码语言:javascript
复制
<property name="methodType">
    <map key-type="java.lang.String">
        <!-- read -->
        <entry key="master" value="find,get,select,count,list,query,stat,show,mine,all,rank,fetch"/>
        <!-- write -->
        <entry key="slave" value="save,insert,add,create,update,delete,remove,gain"/>
    </map>
</property>
读写动态库配置
代码语言:javascript
复制
<bean id="fwmarketDataSource" class="com.jd.fwmarket.datasource.DynamicDataSource" lazy-init="true">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="master" value-ref="masterDB"/>
            <entry key="slave" value-ref="slaveDB"/>
        </map>
    </property>
    <!-- 设置默认的数据源,这里默认走写库 -->
    <property name="defaultTargetDataSource" ref="masterDB"/>
</bean>
DynamicDataSource:

定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可,由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。

代码语言:javascript
复制
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
        return DynamicDataSourceHolder.getDataSourceKey();
    }
}
DynamicDataSourceHolder类:
代码语言:javascript
复制
public class DynamicDataSourceHolder {
 
    // 写库对应的数据源key
    private static final String MASTER= "master";
 
    // 读库对应的数据源key
    private static final String SLAVE = "slave";
 
    // 使用ThreadLocal记录当前线程的数据源key
    private static final ThreadLocal<String> holder = new ThreadLocal<String>();
 
    public static void putDataSourceKey(String key) {
        holder.set(key);
    }
 
    public static String getDataSourceKey() {
        return holder.get();
    }
 
    public static void markDBMaster(){
        putDataSourceKey(MASTER);
    }
 
    public static void markDBSlave(){
        putDataSourceKey(SLAVE);
    }
 
    public static void markClear(){
        putDataSourceKey(null);
    }
}

动态设置数据源可以通过Spring AOP来实现,而AOP切面的方式也有很多种。

Spring AOP的原理:Spring AOP采用动态代理实现,在Spring容器中的bean会被代理对象代替,代理对象里加入了增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截。

事务切面和读/写库选择切面

代码语言:javascript
复制
<bean id="dataSourceAspect" class="com.jd.fwmarket.service.datasource.DataSourceAspect"/>
<aop:config>
    <aop:pointcut id="txPointcut" expression="execution(* com.jd.fwmarket.dao..*Impl.*(..))"/>
    <!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高执行 -->
    <aop:aspect ref="dataSourceAspect" order="-9999">
        <aop:before method="before" pointcut-ref="txPointcut"/>
        <aop:after method="after" pointcut-ref="txPointcut"/>
    </aop:aspect>
</aop:config>
Java逻辑:
代码语言:javascript
复制
public class DataSourceAspect {
 
    private static final String[] defaultSlaveMethodStart 
                    = new String[]{"query", "find", "get", "select", "count", "list"};
 
    /**
     * 在进入Dao方法之前执行
     *
     * @param point 切面对象
     */
    public void before(JoinPoint point) {
        String methodName = point.getSignature().getName();
 
        boolean isSlave = isSlave(methodName);
        if (isSlave) {
            DynamicDataSourceHolder.markDBSlave();
        } else {
            DynamicDataSourceHolder.markDBMaster();
        }
    }
 
    public void after(){
        DynamicDataSourceHolder.markClear();
    }
}
使用BeanNameAutoProxyCreator创建代理
代码语言:javascript
复制
<bean id="MySqlDaoSourceInterceptor" class="com.jd.fwmarket.dao.aop.DaoSourceInterceptor">
    <property name="dbType" value="mysql"/>
    <property name="packageName" value="com.jd.fwmarket"/>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
        <value>*Mapper</value>
    </property>
    <property name="interceptorNames">
        <list>
            <value>MySqlDaoSourceInterceptor</value>
        </list>
    </property>
</bean>
Java逻辑:
代码语言:javascript
复制
public class DaoSourceInterceptor implements MethodInterceptor {
 
    public Object invoke(MethodInvocation invocation) throws Throwable {
        dataSourceAspect(invocation);
        Object result = invocation.proceed();
        DataSourceHandler.putDataSource(null);
        return result;
    }
 
    private void dataSourceAspect(MethodInvocation invocation) {
        String method = invocation.getMethod().getName();
        for (String key : ChooseDataSource.METHOD_TYPE_MAP.keySet()) {
            for (String type : ChooseDataSource.METHOD_TYPE_MAP.get(key)) {
                if (method.startsWith(type)) {
                    DataSourceHandler.putDataSource(key);
                    return;
                }
            }
        }
    }
}

Spring的事务处理为了与数据访问解耦,它提供了一套处理数据资源的机制,而这个机制采用ThreadLocal的方式。

事务管理器

Spring中通常通过@Transactional来声明使用事务。如果@Transactional不指定事务管理器,使用缺省。注意如果Spring容器中定义了两个事务管理器,@Transactional标注是不支持区分使用哪个事务管理器的,Spring 3.0之后的版本Transactional增加了个string类型的value属性来特殊指定加以区分。

代码语言:javascript
复制
@Transactional
public int insertEntryCreateId(UrpMenu urpMenu) {
    urpMenu.setMId(this.sequenceUtil.get(SequenceConstants.MARKET_URP_MENU));
    return super.insertEntryCreateId(urpMenu);
}

同时进行XML配置

代码语言:javascript
复制
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<bean id="transactionManager" 
                class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="fwmarketDataSource"/>
</bean>

其中dataSource是在Spring配置文件中定义的数据源的对象实例。transaction-manager属性保存一个对在Spring配置文件中定义的事务管理器bean的引用,如果没有它,就会忽略@Transactional注释,导致代码不会使用任何事务。proxy-target-class控制是基于接口的还是基于类的代理被创建,如果属性值被设置为true,那么基于类的代理将起作用,如果属性值为false或者被省略,那么标准的JDK基于接口的代理将起作用。

注意@Transactional建议在具体的类(或类的方法)上使用,不要使用在类所要实现的任何接口上。

(推荐阅读:Spring事务隔离级别和传播特性 http://www.cnblogs.com/zhishan/p/3195219.html

SQL四类隔离级别

事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。Mysql中支持事务的存储引擎有InnoDB和NDB。InnoDB是mysql默认的存储引擎,默认的隔离级别是RR(Repeatable Read)。

事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现。

(推荐阅读:数据库事务与MySQL事务总结 https://zhuanlan.zhihu.com/p/29166694

Q1 在读写分离时会不会造成事务主从切换错误

一个线程在Serivcie时Select时选择的是从库,DynamicDataSourceHolder中ThreadLocal对应线程存储的是slave,然后调用Manager时进入事务,事务使用默认的transacatinManager关联的dataSource,而此时会不会获取到的是slave?

经验证不会,但这是因为在AOP设置动态织出的时候,都要清空DynamicDataSourceHolder的ThreadLocal,如此避免了数据库事务传播行为影响的主从切换错误。如果Selelct DB从库完成之后不清空ThreadLocal,那么ThreadLocal跟线程绑定就会传播到Transaction,造成事务操作从库异常。而清空ThreadLocal之后,Spring的事务拦截先于动态数据源的判断,所以事务会切换成主库,即使事务中再有查询从库的操作,也不会造成主库事务异常。

Q2 事务隔离级别和传播特性会不会影响数据连接池死锁

一个线程在Service层Select数据会从数据库获取一个Connection,通常来讲,后续DB的操作在同一线线程会复用这个DB Connection,但是从Service进入Manager的事务后,Get Seq获取全局唯一标识,所以Get Seq一般都会开启新的事物从DB Pool里重新获取一个新连接进行操作,但是问题是如果两个事务关联的datasource是同一个,即DB Pool是同一个,那么如果DB Pool已经为空,是否会造成死锁?

经验证会死锁,所以在实践过程中,如果有此实现,建议Get Seq不要使用与事务同一个连接池。或者采用事务隔离级别设置PROPAGATION_REQUIRES_NEW进行处理。最优的实践是宎把Get SeqId放到事务里处理。

总结

分析的不是很深,有很多地方还不是特别了解,欢迎吐槽相互学习,尤其是说错了的地方,一定请帮忙指正,以免误人子弟。

作者:张松然

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 在读写分离时会不会造成事务主从切换错误
  • 2. 事务隔离级别和传播特性会不会影响数据连接池死锁
  • 事务切面和读/写库选择切面
  • 事务管理器
  • SQL四类隔离级别
  • Q1 在读写分离时会不会造成事务主从切换错误
  • Q2 事务隔离级别和传播特性会不会影响数据连接池死锁
  • 总结
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档