一个线程在Serivcie时Select时选择的是从库,DynamicDataSourceHolder中ThreadLocal对应线程存储的是slave,而后调用Manager时进入事务,事务使用默认的transacatinManager关联的dataSource,而此时会不会获取到的是slave?javascript
一个线程在Service层Select数据会从数据库获取一个Connection,一般来说,后续DB的操做在同一线线程会复用这个DB Connection,可是从Service进入Manager的事务后,Get Seq获取全局惟一标识,因此Get Seq通常都会开启新的事物从DB Pool里从新获取一个新链接进行操做,可是问题是若是两个事务关联的datasource是同一个,即DB Pool是同一个,那么若是DB Pool已经为空,是否会形成死锁?html
为了减轻数据库的压力,通常会进行数据库的读写分离,实现方法一是经过分析sql语句是insert/select/update/delete中的哪种,从而对应选择主从,二是经过拦截方法名称的方式来决定主从的,如:savejava
一般在方法上标上自定义标签来选择主从。mysql
@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>
复制代码
定义动态数据源,实现经过集成Spring提供的AbstractRoutingDataSource,只须要实现determineCurrentLookupKey方法便可,因为DynamicDataSource是单例的,线程不安全的,因此采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。spring
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 使用DynamicDataSourceHolder保证线程安全,而且获得当前线程中的数据源key
return DynamicDataSourceHolder.getDataSourceKey();
}
}
复制代码
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切面的方式也有不少种。sql
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>
复制代码
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();
}
}
复制代码
<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> 复制代码
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的方式。express
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事务隔离级别和传播特性 www.cnblogs.com/zhishan/p/3…)
事务的实现是基于数据库的存储引擎。不一样的存储引擎对事务的支持程度不同。Mysql中支持事务的存储引擎有InnoDB和NDB。InnoDB是mysql默认的存储引擎,默认的隔离级别是RR(Repeatable Read)。
事务的隔离性是经过锁实现,而事务的原子性、一致性和持久性则是经过事务日志实现。
(推荐阅读:数据库事务与MySQL事务总结 zhuanlan.zhihu.com/p/29166694)
一个线程在Serivcie时Select时选择的是从库,DynamicDataSourceHolder中ThreadLocal对应线程存储的是slave,而后调用Manager时进入事务,事务使用默认的transacatinManager关联的dataSource,而此时会不会获取到的是slave?
经验证不会,但这是由于在AOP设置动态织出的时候,都要清空DynamicDataSourceHolder的ThreadLocal,如此避免了数据库事务传播行为影响的主从切换错误。若是Selelct DB从库完成以后不清空ThreadLocal,那么ThreadLocal跟线程绑定就会传播到Transaction,形成事务操做从库异常。而清空ThreadLocal以后,Spring的事务拦截先于动态数据源的判断,因此事务会切换成主库,即便事务中再有查询从库的操做,也不会形成主库事务异常。
一个线程在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性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。
做者:张松然
京东商城,商家研发部架构师