美文网首页
数据库读写分离与事务纠缠的那点坑

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

作者: Frank_cfe2 | 来源:发表于2020-05-14 17:40 被阅读0次

    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库。

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

    @DataSource("slave")intqueryForCount(OrderQueryConditionqueryCondition);

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

    <propertyname="methodType">

    <mapkey-type="java.lang.String">

    <!-- read -->

    <entrykey="master"value="find,get,select,count,list,query,stat,show,mine,all,rank,fetch"/>

    <!-- write -->

    <entrykey="slave"value="save,insert,add,create,update,delete,remove,gain"/></map>

    </property>

    读写动态库配置

    <beanid="fwmarketDataSource"class="com.jd.fwmarket.datasource.DynamicDataSource"lazy-init="true"><propertyname="targetDataSources"><mapkey-type="java.lang.String"><entrykey="master"value-ref="masterDB"/><entrykey="slave"value-ref="slaveDB"/></map></property><!-- 设置默认的数据源,这里默认走写库 --><propertyname="defaultTargetDataSource"ref="masterDB"/></bean>

    DynamicDataSource:

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

    publicclassDynamicDataSourceextendsAbstractRoutingDataSource{@OverrideprotectedObjectdetermineCurrentLookupKey(){// 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源keyreturnDynamicDataSourceHolder.getDataSourceKey();}}

    DynamicDataSourceHolder类:

    publicclassDynamicDataSourceHolder{// 写库对应的数据源keyprivatestaticfinalStringMASTER="master";// 读库对应的数据源keyprivatestaticfinalStringSLAVE="slave";// 使用ThreadLocal记录当前线程的数据源keyprivatestaticfinalThreadLocal<String>holder=newThreadLocal<String>();publicstaticvoidputDataSourceKey(Stringkey){holder.set(key);}publicstaticStringgetDataSourceKey(){returnholder.get();}publicstaticvoidmarkDBMaster(){putDataSourceKey(MASTER);}publicstaticvoidmarkDBSlave(){putDataSourceKey(SLAVE);}publicstaticvoidmarkClear(){putDataSourceKey(null);}}

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

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

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

    <beanid="dataSourceAspect"class="com.jd.fwmarket.service.datasource.DataSourceAspect"/><aop:config><aop:pointcutid="txPointcut"expression="execution(* com.jd.fwmarket.dao..*Impl.*(..))"/><!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高执行 --><aop:aspectref="dataSourceAspect"order="-9999"><aop:beforemethod="before"pointcut-ref="txPointcut"/><aop:aftermethod="after"pointcut-ref="txPointcut"/></aop:aspect></aop:config>

    Java逻辑:

    publicclassDataSourceAspect{privatestaticfinalString[]defaultSlaveMethodStart=newString[]{"query","find","get","select","count","list"};/**

        * 在进入Dao方法之前执行

        *

        * @param point 切面对象

        */publicvoidbefore(JoinPointpoint){StringmethodName=point.getSignature().getName();booleanisSlave=isSlave(methodName);if(isSlave){DynamicDataSourceHolder.markDBSlave();}else{DynamicDataSourceHolder.markDBMaster();}}publicvoidafter(){DynamicDataSourceHolder.markClear();}}

    使用BeanNameAutoProxyCreator创建代理

    <beanid="MySqlDaoSourceInterceptor"class="com.jd.fwmarket.dao.aop.DaoSourceInterceptor">    <propertyname="dbType"value="mysql"/>    <propertyname="packageName"value="com.jd.fwmarket"/></bean><beanclass="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">    <propertyname="beanNames">        <value>*Mapper</value>    </property>    <propertyname="interceptorNames">        <list>            <value>MySqlDaoSourceInterceptor</value>        </list>    </property></bean>

    Java逻辑:

    publicclassDaoSourceInterceptorimplementsMethodInterceptor{publicObjectinvoke(MethodInvocation invocation)throws Throwable{dataSourceAspect(invocation);Object result=invocation.proceed();DataSourceHandler.putDataSource(null);returnresult;}privatevoiddataSourceAspect(MethodInvocation invocation){String method=invocation.getMethod().getName();for(String key:ChooseDataSource.METHOD_TYPE_MAP.keySet()){for(Stringtype: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属性来特殊指定加以区分。

    @TransactionalpublicintinsertEntryCreateId(UrpMenu urpMenu){urpMenu.setMId(this.sequenceUtil.get(SequenceConstants.MARKET_URP_MENU));returnsuper.insertEntryCreateId(urpMenu);}

    同时进行XML配置

    <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放到事务里处理。

    总结

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

    在此我向大家推荐一个架构学习交流群。交流学习群号:833145934 里面资深架构师会分享一些整理好的录制视频录像和BATJ面试题:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。

    作者:AI乔治

    链接:https://www.jianshu.com/p/8b43bbf52dc6

    来源:简书

    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

          本文标题:数据库读写分离与事务纠缠的那点坑

          本文链接:https://www.haomeiwen.com/subject/xrwemxtx.html