119

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

 6 years ago
source link: http://www.linkedkeeper.com/detail/blog.action?bid=1043
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

张松然

作者

京东商城,商家研发部架构师。丰富的构建高性能高可用大规模分布式系统的研发、架构经验。2013年加入京东,目前负责京麦服务网关和京麦服务市场的系统研发工作。

本篇文章讨论在数据库读写分离时使用事务的那些坑:

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

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

linkedkeeper0_96d044f6-0add-4755-80ba-39f7cec48a43.jpg

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

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

linkedkeeper0_c05add0d-f3b1-4f8a-a82c-85d38b5cbd14.jpg

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

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

@DataSource("slave")
int queryForCount(OrderQueryCondition queryCondition);

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

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

读写动态库配置   

<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完成。

public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
return DynamicDataSourceHolder.getDataSourceKey();
}
}

DynamicDataSourceHolder类:

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会被代理对象代替,代理对象里加入了增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截。

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

<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逻辑:

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创建代理

<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逻辑:

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属性来特殊指定加以区分。

@Transactional
public int insertEntryCreateId(UrpMenu urpMenu) {
urpMenu.setMId(this.sequenceUtil.get(SequenceConstants.MARKET_URP_MENU));
return super.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的事务拦截先于动态数据源的判断,所以事务会切换成主库,即使事务中再有查询从库的操作,也不会造成主库事务异常。

linkedkeeper0_96d044f6-0add-4755-80ba-39f7cec48a43.jpg

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

linkedkeeper0_c05add0d-f3b1-4f8a-a82c-85d38b5cbd14.jpg

总结

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

本文受原创保护,未经作者授权,禁止转载。 linkedkeeper.com (文/张松然)  ©著作权归作者所有

linkedkeeper0_8ec00ecc-381d-4c7e-b04d-c2edcf7f849a.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK