国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

Spring+MyBatis實現(xiàn)數(shù)據(jù)庫讀寫分離方案

共 49063字,需瀏覽 99分鐘

 ·

2021-07-03 19:56

方案1

通過MyBatis配置文件創(chuàng)建讀寫分離兩個DataSource,每個SqlSessionFactoryBean對象的mapperLocations屬性制定兩個讀寫數(shù)據(jù)源的配置文件。將所有讀的操作配置在讀文件中,所有寫的操作配置在寫文件中。

  • 優(yōu)點:實現(xiàn)簡單

  • 缺點:維護麻煩,需要對原有的xml文件進行重新修改,不支持多讀,不易擴展

  • 實現(xiàn)方式

<bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
destroy-method="close">

<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 配置獲取連接等待超時的時間 -->
<property name="maxWait" value="60000"/>
<!-- 配置間隔多久才進行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<!-- 打開PSCache,并且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
<property name="filters" value="config"/>
<property name="connectionProperties" value="config.decrypt=true" />
</bean>

<bean id="readDataSource" parent="abstractDataSource">
<!-- 基本屬性 url、user、password -->
<property name="url" value="${read.jdbc.url}"/>
<property name="username" value="${read.jdbc.user}"/>
<property name="password" value="${read.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${read.jdbc.initPoolSize}"/>
<property name="minIdle" value="10"/>
<property name="maxActive" value="${read.jdbc.maxPoolSize}"/>
</bean>

<bean id="writeDataSource" parent="abstractDataSource">
<!-- 基本屬性 url、user、password -->
<property name="url" value="${write.jdbc.url}"/>
<property name="username" value="${write.jdbc.user}"/>
<property name="password" value="${write.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${write.jdbc.initPoolSize}"/>
<property name="minIdle" value="10"/>
<property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
</bean>

<bean id="readSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 實例化sqlSessionFactory時需要使用上述配置好的數(shù)據(jù)源以及SQL映射文件 -->
<property name="dataSource" ref="readDataSource"/>
<property name="mapperLocations" value="classpath:mapper/read/*.xml"/>
</bean>

<bean id="writeSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 實例化sqlSessionFactory時需要使用上述配置好的數(shù)據(jù)源以及SQL映射文件 -->
<property name="dataSource" ref="writeDataSource"/>
<property name="mapperLocations" value="classpath:mapper/write/*.xml"/>
</bean>
復(fù)制代碼
方案2

通過Spring AOP在業(yè)務(wù)層實現(xiàn)讀寫分離,在DAO層調(diào)用前定義切面,利用Spring的AbstractRoutingDataSource解決多數(shù)據(jù)源的問題,實現(xiàn)動態(tài)選擇數(shù)據(jù)源

  • 優(yōu)點:通過注解的方法在DAO每個方法上配置數(shù)據(jù)源,原有代碼改動量少,易擴展,支持多讀

  • 缺點:需要在DAO每個方法上配置注解,人工管理,容易出錯

  • 實現(xiàn)方式

//定義枚舉類型,讀寫
public enum DynamicDataSourceGlobal {
READ, WRITE;
}
復(fù)制代碼
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* RUNTIME
* 定義注解
* 編譯器將把注釋記錄在類文件中,在運行時 VM 將保留注釋,因此可以反射性地讀取。
* @author shma1664
*
*/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {

public DynamicDataSourceGlobal value() default DynamicDataSourceGlobal.READ;

}
復(fù)制代碼
/**
* Created by IDEA
* 本地線程設(shè)置和獲取數(shù)據(jù)源信息
* User: mashaohua
* Date: 2016-07-07 13:35
* Desc:
*/

public class DynamicDataSourceHolder {

private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>();

public static void putDataSource(DynamicDataSourceGlobal dataSource){
holder.set(dataSource);
}

public static DynamicDataSourceGlobal getDataSource(){
return holder.get();
}

public static void clearDataSource() {
holder.remove();
}

}
復(fù)制代碼
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* Created by IDEA
* User: mashaohua
* Date: 2016-07-14 10:56
* Desc: 動態(tài)數(shù)據(jù)源實現(xiàn)讀寫分離
*/

public class DynamicDataSource extends AbstractRoutingDataSource {

private Object writeDataSource; //寫數(shù)據(jù)源

private List<Object> readDataSources; //多個讀數(shù)據(jù)源

private int readDataSourceSize; //讀數(shù)據(jù)源個數(shù)

private int readDataSourcePollPattern = 0; //獲取讀數(shù)據(jù)源方式,0:隨機,1:輪詢

private AtomicLong counter = new AtomicLong(0);

private static final Long MAX_POOL = Long.MAX_VALUE;

private final Lock lock = new ReentrantLock();

@Override
public void afterPropertiesSet() {
if (this.writeDataSource == null) {
throw new IllegalArgumentException("Property 'writeDataSource' is required");
}
setDefaultTargetDataSource(writeDataSource);
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource);
if (this.readDataSources == null) {
readDataSourceSize = 0;
} else {
for(int i=0; i<readDataSources.size(); i++) {
targetDataSources.put(DynamicDataSourceGlobal.READ.name() + i, readDataSources.get(i));
}
readDataSourceSize = readDataSources.size();
}
setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}

@Override
protected Object determineCurrentLookupKey() {

DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();

if(dynamicDataSourceGlobal == null
|| dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE
|| readDataSourceSize <= 0) {
return DynamicDataSourceGlobal.WRITE.name();
}

int index = 1;

if(readDataSourcePollPattern == 1) {
//輪詢方式
long currValue = counter.incrementAndGet();
if((currValue + 1) >= MAX_POOL) {
try {
lock.lock();
if((currValue + 1) >= MAX_POOL) {
counter.set(0);
}
} finally {
lock.unlock();
}
}
index = (int) (currValue % readDataSourceSize);
} else {
//隨機方式
index = ThreadLocalRandom.current().nextInt(0, readDataSourceSize);
}
return dynamicDataSourceGlobal.name() + index;
}

public void setWriteDataSource(Object writeDataSource) {
this.writeDataSource = writeDataSource;
}

public void setReadDataSources(List<Object> readDataSources) {
this.readDataSources = readDataSources;
}

public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
this.readDataSourcePollPattern = readDataSourcePollPattern;
}
}
復(fù)制代碼
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;

/**
* Created by IDEA
* User: mashaohua
* Date: 2016-07-07 13:39
* Desc: 定義選擇數(shù)據(jù)源切面
*/

public class DynamicDataSourceAspect {

private static final Logger logger = Logger.getLogger(DynamicDataSourceAspect.class);

public void pointCut(){};

public void before(JoinPoint point)
{
Object target = point.getTarget();
String methodName = point.getSignature().getName();
Class<?>[] clazz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
try {
Method method = clazz[0].getMethod(methodName, parameterTypes);
if (method != null && method.isAnnotationPresent(DataSource.class)) {
DataSource data = method.getAnnotation(DataSource.class);
DynamicDataSourceHolder.putDataSource(data.value());
}
} catch (Exception e) {
logger.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, e.getMessage()));
}
}

public void after(JoinPoint point) {
DynamicDataSourceHolder.clearDataSource();
}
}
復(fù)制代碼
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"
>


<bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 配置獲取連接等待超時的時間 -->
<property name="maxWait" value="60000"/>
<!-- 配置間隔多久才進行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<!-- 打開PSCache,并且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
<property name="filters" value="config"/>
<property name="connectionProperties" value="config.decrypt=true" />
</bean>

<bean id="dataSourceRead1" parent="abstractDataSource">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 基本屬性 url、user、password -->
<property name="url" value="${read1.jdbc.url}"/>
<property name="username" value="${read1.jdbc.user}"/>
<property name="password" value="${read1.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${read1.jdbc.initPoolSize}"/>
<property name="minIdle" value="${read1.jdbc.minPoolSize}"/>
<property name="maxActive" value="${read1.jdbc.maxPoolSize}"/>
</bean>

<bean id="dataSourceRead2" parent="abstractDataSource">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 基本屬性 url、user、password -->
<property name="url" value="${read2.jdbc.url}"/>
<property name="username" value="${read2.jdbc.user}"/>
<property name="password" value="${read2.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${read2.jdbc.initPoolSize}"/>
<property name="minIdle" value="${read2.jdbc.minPoolSize}"/>
<property name="maxActive" value="${read2.jdbc.maxPoolSize}"/>
</bean>

<bean id="dataSourceWrite" parent="abstractDataSource">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 基本屬性 url、user、password -->
<property name="url" value="${write.jdbc.url}"/>
<property name="username" value="${write.jdbc.user}"/>
<property name="password" value="${write.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${write.jdbc.initPoolSize}"/>
<property name="minIdle" value="${write.jdbc.minPoolSize}"/>
<property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
</bean>

<bean id="dataSource" class="com.test.api.dao.datasource.DynamicDataSource">
<property name="writeDataSource" ref="dataSourceWrite" />
<property name="readDataSources">
<list>
<ref bean="dataSourceRead1" />
<ref bean="dataSourceRead2" />
</list>
</property>
<!--輪詢方式-->
<property name="readDataSourcePollPattern" value="1" />
<property name="defaultTargetDataSource" ref="dataSourceWrite"/>
</bean>

<tx:annotation-driven transaction-manager="transactionManager"/>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 針對myBatis的配置項 -->
<!-- 配置sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 實例化sqlSessionFactory時需要使用上述配置好的數(shù)據(jù)源以及SQL映射文件 -->
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

<!-- 配置掃描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 掃描包以及它的子包下的所有映射接口類 -->
<property name="basePackage" value="com.test.api.dao.inte"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

<!-- 配置數(shù)據(jù)庫注解aop -->
<bean id="dynamicDataSourceAspect" class="com.test.api.dao.datasource.DynamicDataSourceAspect" />
<aop:config>
<aop:aspect id="c" ref="dynamicDataSourceAspect">
<aop:pointcut id="tx" expression="execution(* com.test.api.dao.inte..*.*(..))"/>
<aop:before pointcut-ref="tx" method="before"/>
<aop:after pointcut-ref="tx" method="after"/>
</aop:aspect>
</aop:config>
<!-- 配置數(shù)據(jù)庫注解aop -->
</beans>
復(fù)制代碼
方案3

通過Mybatis的Plugin在業(yè)務(wù)層實現(xiàn)數(shù)據(jù)庫讀寫分離,在MyBatis創(chuàng)建Statement對象前通過攔截器選擇真正的數(shù)據(jù)源,在攔截器中根據(jù)方法名稱不同(select、update、insert、delete)選擇數(shù)據(jù)源。

  • 優(yōu)點:原有代碼不變,支持多讀,易擴展

  • 缺點:

  • 實現(xiàn)方式

/**
* Created by IDEA
* User: mashaohua
* Date: 2016-07-19 15:40
* Desc: 創(chuàng)建Connection代理接口
*/

public interface ConnectionProxy extends Connection {

/**
* 根據(jù)傳入的讀寫分離需要的key路由到正確的connection
* @param key 數(shù)據(jù)源標識
* @return
*/

Connection getTargetConnection(String key);
}
復(fù)制代碼
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import javax.sql.DataSource;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.Assert;

public abstract class AbstractDynamicDataSourceProxy extends AbstractDataSource implements InitializingBean {

private List<Object> readDataSources;
private List<DataSource> resolvedReadDataSources;

private Object writeDataSource;
private DataSource resolvedWriteDataSource;

private int readDataSourcePollPattern = 0;

private int readDsSize;

private boolean defaultAutoCommit = true;
private int defaultTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED;

public static final String READ = "read";

public static final String WRITE = "write";

private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

@Override
public Connection getConnection() throws SQLException {
return (Connection) Proxy.newProxyInstance(
com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class.getClassLoader(),
new Class[] {com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class},
new RWConnectionInvocationHandler());
}

@Override
public Connection getConnection(String username, String password)
throws SQLException
{
return (Connection) Proxy.newProxyInstance(
com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class.getClassLoader(),
new Class[] {com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class},
new RWConnectionInvocationHandler(username,password));
}

public int getReadDsSize(){
return readDsSize;
}

public List<DataSource> getResolvedReadDataSources() {
return resolvedReadDataSources;
}

public void afterPropertiesSet() throws Exception {

if(writeDataSource == null){
throw new IllegalArgumentException("Property 'writeDataSource' is required");
}
this.resolvedWriteDataSource = resolveSpecifiedDataSource(writeDataSource);

resolvedReadDataSources = new ArrayList<DataSource>(readDataSources.size());
for(Object item : readDataSources){
resolvedReadDataSources.add(resolveSpecifiedDataSource(item));
}
readDsSize = readDataSources.size();
}

protected DataSource determineTargetDataSource(String key) {
Assert.notNull(this.resolvedReadDataSources, "DataSource router not initialized");
if(WRITE.equals(key)){
return resolvedWriteDataSource;
}else{
return loadReadDataSource();
}
}

public Logger getParentLogger() {
// NOOP Just ignore
return null;
}

/**
* 獲取真實的data source
* @param dataSource (jndi | real data source)
* @return
* @throws IllegalArgumentException
*/

protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource) dataSource;
}
else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String) dataSource);
}
else {
throw new IllegalArgumentException(
"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}

protected abstract DataSource loadReadDataSource();

public void setReadDsSize(int readDsSize) {
this.readDsSize = readDsSize;
}

public List<Object> getReadDataSources() {
return readDataSources;
}

public void setReadDataSources(List<Object> readDataSources) {
this.readDataSources = readDataSources;
}

public Object getWriteDataSource() {
return writeDataSource;
}

public void setWriteDataSource(Object writeDataSource) {
this.writeDataSource = writeDataSource;
}

public void setResolvedReadDataSources(List<DataSource> resolvedReadDataSources) {
this.resolvedReadDataSources = resolvedReadDataSources;
}

public DataSource getResolvedWriteDataSource() {
return resolvedWriteDataSource;
}

public void setResolvedWriteDataSource(DataSource resolvedWriteDataSource) {
this.resolvedWriteDataSource = resolvedWriteDataSource;
}

public int getReadDataSourcePollPattern() {
return readDataSourcePollPattern;
}

public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
this.readDataSourcePollPattern = readDataSourcePollPattern;
}

/**
* Invocation handler that defers fetching an actual JDBC Connection
* until first creation of a Statement.
*/

private class RWConnectionInvocationHandler implements InvocationHandler {

private String username;

private String password;

private Boolean readOnly = Boolean.FALSE;

private Integer transactionIsolation;

private Boolean autoCommit;

private boolean closed = false;

private Connection target;

public RWConnectionInvocationHandler() {

}

public RWConnectionInvocationHandler(String username, String password) {
this();
this.username = username;
this.password = password;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Invocation on ConnectionProxy interface coming in...

if (method.getName().equals("equals")) {
// We must avoid fetching a target Connection for "equals".
// Only consider equal when proxies are identical.
return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
}
else if (method.getName().equals("hashCode")) {
// We must avoid fetching a target Connection for "hashCode",
// and we must return the same hash code even when the target
// Connection has been fetched: use hashCode of Connection proxy.
return new Integer(System.identityHashCode(proxy));
}
else if (method.getName().equals("getTargetConnection")) {
// Handle getTargetConnection method: return underlying connection.
return getTargetConnection(method,args);
}

if (!hasTargetConnection()) {
// No physical target Connection kept yet ->
// resolve transaction demarcation methods without fetching
// a physical JDBC Connection until absolutely necessary.

if (method.getName().equals("toString")) {
return "RW Routing DataSource Proxy";
}
else if (method.getName().equals("isReadOnly")) {
return this.readOnly;
}
else if (method.getName().equals("setReadOnly")) {
this.readOnly = (Boolean) args[0];
return null;
}
else if (method.getName().equals("getTransactionIsolation")) {
if (this.transactionIsolation != null) {
return this.transactionIsolation;
}
return defaultTransactionIsolation;
// Else fetch actual Connection and check there,
// because we didn't have a default specified.
}
else if (method.getName().equals("setTransactionIsolation")) {
this.transactionIsolation = (Integer) args[0];
return null;
}
else if (method.getName().equals("getAutoCommit")) {
if (this.autoCommit != null)
return this.autoCommit;
return defaultAutoCommit;
// Else fetch actual Connection and check there,
// because we didn't have a default specified.
}
else if (method.getName().equals("setAutoCommit")) {
this.autoCommit = (Boolean) args[0];
return null;
}
else if (method.getName().equals("commit")) {
// Ignore: no statements created yet.
return null;
}
else if (method.getName().equals("rollback")) {
// Ignore: no statements created yet.
return null;
}
else if (method.getName().equals("getWarnings")) {
return null;
}
else if (method.getName().equals("clearWarnings")) {
return null;
}
else if (method.getName().equals("isClosed")) {
return (this.closed ? Boolean.TRUE : Boolean.FALSE);
}
else if (method.getName().equals("close")) {
// Ignore: no target connection yet.
this.closed = true;
return null;
}
else if (this.closed) {
// Connection proxy closed, without ever having fetched a
// physical JDBC Connection: throw corresponding SQLException.
throw new SQLException("Illegal operation: connection is closed");
}
}

// Target Connection already fetched,
// or target Connection necessary for current operation ->
// invoke method on target connection.
try {
return method.invoke(target, args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}

/**
* Return whether the proxy currently holds a target Connection.
*/

private boolean hasTargetConnection() {
return (this.target != null);
}

/**
* Return the target Connection, fetching it and initializing it if necessary.
*/

private Connection getTargetConnection(Method operation,Object[] args) throws SQLException {

if (this.target == null) {
String key = (String) args[0];
// No target Connection held -> fetch one.
if (logger.isDebugEnabled()) {
logger.debug("Connecting to database for operation '" + operation.getName() + "'");
}

// Fetch physical Connection from DataSource.
this.target = (this.username != null) ?
determineTargetDataSource(key).getConnection(this.username, this.password) :
determineTargetDataSource(key).getConnection();

// If we still lack default connection properties, check them now.
//checkDefaultConnectionProperties(this.target);

// Apply kept transaction settings, if any.
if (this.readOnly.booleanValue()) {
this.target.setReadOnly(this.readOnly.booleanValue());
}
if (this.transactionIsolation != null) {
this.target.setTransactionIsolation(this.transactionIsolation.intValue());
}
if (this.autoCommit != null && this.autoCommit.booleanValue() != this.target.getAutoCommit()) {
this.target.setAutoCommit(this.autoCommit.booleanValue());
}
}

else {
// Target Connection already held -> return it.
if (logger.isDebugEnabled()) {
logger.debug("Using existing database connection for operation '" + operation.getName() + "'");
}
}

return this.target;
}
}

}
復(fù)制代碼
import javax.sql.DataSource;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* Created by IDEA
* User: mashaohua
* Date: 2016-07-19 16:04
* Desc:
*/

public class DynamicRoutingDataSourceProxy extends AbstractDynamicDataSourceProxy {

private AtomicLong counter = new AtomicLong(0);

private static final Long MAX_POOL = Long.MAX_VALUE;

private final Lock lock = new ReentrantLock();

@Override
protected DataSource loadReadDataSource() {
int index = 1;

if(getReadDataSourcePollPattern() == 1) {
//輪詢方式
long currValue = counter.incrementAndGet();
if((currValue + 1) >= MAX_POOL) {
try {
lock.lock();
if((currValue + 1) >= MAX_POOL) {
counter.set(0);
}
} finally {
lock.unlock();
}
}
index = (int) (currValue % getReadDsSize());
} else {
//隨機方式
index = ThreadLocalRandom.current().nextInt(0, getReadDsSize());
}
return getResolvedReadDataSources().get(index);
}
}
復(fù)制代碼
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;

import java.sql.Connection;
import java.util.Properties;

/**
* 攔截器
*/

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class DynamicPlugin implements Interceptor {

public Object intercept(Invocation invocation) throws Throwable {


Connection conn = (Connection)invocation.getArgs()[0];
//如果是采用了我們代理,則路由數(shù)據(jù)源
if(conn instanceof com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy){
StatementHandler statementHandler = (StatementHandler) invocation
.getTarget();

MappedStatement mappedStatement = null;
if (statementHandler instanceof RoutingStatementHandler) {
StatementHandler delegate = (StatementHandler) ReflectionUtils
.getFieldValue(statementHandler, "delegate");
mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(
delegate, "mappedStatement");
} else {
mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(
statementHandler, "mappedStatement");
}
String key = AbstractDynamicDataSourceProxy.WRITE;

if(mappedStatement.getSqlCommandType() == SqlCommandType.SELECT){
key = AbstractDynamicDataSourceProxy.READ;
}else{
key = AbstractDynamicDataSourceProxy.WRITE;
}

ConnectionProxy connectionProxy = (ConnectionProxy)conn;
connectionProxy.getTargetConnection(key);

}

return invocation.proceed();

}

public Object plugin(Object target) {

return Plugin.wrap(target, this);
}

public void setProperties(Properties properties) {
//NOOP

}

}
復(fù)制代碼
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

import java.lang.reflect.*;

public class ReflectionUtils {

private static final Log logger = LogFactory.getLog(ReflectionUtils.class);

/**
* 直接設(shè)置對象屬性值,無視private/protected修飾符,不經(jīng)過setter函數(shù).
*/

public static void setFieldValue(final Object object, final String fieldName, final Object value) {
Field field = getDeclaredField(object, fieldName);

if (field == null)
throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");

makeAccessible(field);

try {
field.set(object, value);
} catch (IllegalAccessException e) {

}
}

/**
* 直接讀取對象屬性值,無視private/protected修飾符,不經(jīng)過getter函數(shù).
*/

public static Object getFieldValue(final Object object, final String fieldName) {
Field field = getDeclaredField(object, fieldName);

if (field == null)
throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");

makeAccessible(field);

Object result = null;
try {
result = field.get(object);
} catch (IllegalAccessException e) {

}
return result;
}

/**
* 直接調(diào)用對象方法,無視private/protected修飾符.
*/

public static Object invokeMethod(final Object object, final String methodName, final Class<?>[] parameterTypes,
final Object[] parameters)
throws InvocationTargetException
{
Method method = getDeclaredMethod(object, methodName, parameterTypes);
if (method == null)
throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]");

method.setAccessible(true);

try {
return method.invoke(object, parameters);
} catch (IllegalAccessException e) {

}

return null;
}

/**
* 循環(huán)向上轉(zhuǎn)型,獲取對象的DeclaredField.
*/

protected static Field getDeclaredField(final Object object, final String fieldName) {
for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass
.getSuperclass()) {
try {
return superClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
}
}
return null;
}

/**
* 循環(huán)向上轉(zhuǎn)型,獲取對象的DeclaredField.
*/

protected static void makeAccessible(final Field field) {
if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
field.setAccessible(true);
}
}

/**
* 循環(huán)向上轉(zhuǎn)型,獲取對象的DeclaredMethod.
*/

protected static Method getDeclaredMethod(Object object, String methodName, Class<?>[] parameterTypes) {
for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass
.getSuperclass()) {
try {
return superClass.getDeclaredMethod(methodName, parameterTypes);
} catch (NoSuchMethodException e) {

}
}
return null;
}

/**
* 通過反射,獲得Class定義中聲明的父類的泛型參數(shù)的類型.
* eg.
* public UserDao extends HibernateDao<User>
*
* @param clazz The class to introspect
* @return the first generic declaration, or Object.class if cannot be determined
*/

@SuppressWarnings("unchecked")
public static <T> Class<T> getSuperClassGenricType(final Class clazz) {
return getSuperClassGenricType(clazz, 0);
}

/**
* 通過反射,獲得Class定義中聲明的父類的泛型參數(shù)的類型.
* eg.
* public UserDao extends HibernateDao<User>
*
* @param clazz The class to introspect
* @return the first generic declaration, or Object.class if cannot be determined
*/

@SuppressWarnings("unchecked")
public static Class getSuperClassGenricType(final Class clazz, final int index) {

Type genType = clazz.getGenericSuperclass();

if (!(genType instanceof ParameterizedType)) {
logger.warn(clazz.getSimpleName() + "'s superclass not ParameterizedType");
return Object.class;
}

Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

if (index >= params.length || index < 0) {
logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: "
+ params.length);
return Object.class;
}
if (!(params[index] instanceof Class)) {
logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
return Object.class;
}

return (Class) params[index];
}

/**
* 將反射時的checked exception轉(zhuǎn)換為unchecked exception.
*/

public static IllegalArgumentException convertToUncheckedException(Exception e) {
if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException
|| e instanceof NoSuchMethodException)
return new IllegalArgumentException("Refelction Exception.", e);
else
return new IllegalArgumentException(e);
}
}
復(fù)制代碼
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD SQL Map Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
<plugins>
<plugin interceptor="com.test.api.dao.mybatis.DynamicPlugin">
</plugin>
</plugins>

</configuration>
復(fù)制代碼
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"
>


<bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 配置獲取連接等待超時的時間 -->
<property name="maxWait" value="60000"/>
<!-- 配置間隔多久才進行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<!-- 打開PSCache,并且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
<property name="filters" value="config"/>
<property name="connectionProperties" value="config.decrypt=true" />
</bean>

<bean id="dataSourceRead1" parent="abstractDataSource">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 基本屬性 url、user、password -->
<property name="url" value="${read1.jdbc.url}"/>
<property name="username" value="${read1.jdbc.user}"/>
<property name="password" value="${read1.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${read1.jdbc.initPoolSize}"/>
<property name="minIdle" value="${read1.jdbc.minPoolSize}"/>
<property name="maxActive" value="${read1.jdbc.maxPoolSize}"/>
</bean>

<bean id="dataSourceRead2" parent="abstractDataSource">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 基本屬性 url、user、password -->
<property name="url" value="${read2.jdbc.url}"/>
<property name="username" value="${read2.jdbc.user}"/>
<property name="password" value="${read2.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${read2.jdbc.initPoolSize}"/>
<property name="minIdle" value="${read2.jdbc.minPoolSize}"/>
<property name="maxActive" value="${read2.jdbc.maxPoolSize}"/>
</bean>

<bean id="dataSourceWrite" parent="abstractDataSource">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<!-- 基本屬性 url、user、password -->
<property name="url" value="${write.jdbc.url}"/>
<property name="username" value="${write.jdbc.user}"/>
<property name="password" value="${write.jdbc.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${write.jdbc.initPoolSize}"/>
<property name="minIdle" value="${write.jdbc.minPoolSize}"/>
<property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
</bean>

<bean id="dataSource" class="com.test.api.dao.datasource.DynamicRoutingDataSourceProxy">
<property name="writeDataSource" ref="dataSourceWrite" />
<property name="readDataSources">
<list>
<ref bean="dataSourceRead1" />
<ref bean="dataSourceRead2" />
</list>
</property>
<!--輪詢方式-->
<property name="readDataSourcePollPattern" value="1" />
</bean>

<tx:annotation-driven transaction-manager="transactionManager"/>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 針對myBatis的配置項 -->
<!-- 配置sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 實例化sqlSessionFactory時需要使用上述配置好的數(shù)據(jù)源以及SQL映射文件 -->
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
<property name="configLocation" value="classpath:mybatis-plugin-config.xml" />
</bean>

<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory" />
</bean>
<!-- 通過掃描的模式,掃描目錄下所有的mapper, 根據(jù)對應(yīng)的mapper.xml為其生成代理類-->
<bean id="mapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.test.api.dao.inte" />
<property name="sqlSessionTemplate" ref="sqlSessionTemplate"></property>
</bean>

</beans>
復(fù)制代碼
方案4

如果你的后臺結(jié)構(gòu)是spring+mybatis,可以通過spring的AbstractRoutingDataSource和mybatis Plugin攔截器實現(xiàn)非常友好的讀寫分離,原有代碼不需要任何改變。推薦第四種方案

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.HashMap;
import java.util.Map;

/**
* Created by IDEA
* User: mashaohua
* Date: 2016-07-14 10:56
* Desc: 動態(tài)數(shù)據(jù)源實現(xiàn)讀寫分離
*/

public class DynamicDataSource extends AbstractRoutingDataSource {

private Object writeDataSource; //寫數(shù)據(jù)源

private Object readDataSource; //讀數(shù)據(jù)源

@Override
public void afterPropertiesSet() {
if (this.writeDataSource == null) {
throw new IllegalArgumentException("Property 'writeDataSource' is required");
}
setDefaultTargetDataSource(writeDataSource);
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource);
if(readDataSource != null) {
targetDataSources.put(DynamicDataSourceGlobal.READ.name(), readDataSource);
}
setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}

@Override
protected Object determineCurrentLookupKey() {

DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();

if(dynamicDataSourceGlobal == null
|| dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) {
return DynamicDataSourceGlobal.WRITE.name();
}

return DynamicDataSourceGlobal.READ.name();
}

public void setWriteDataSource(Object writeDataSource) {
this.writeDataSource = writeDataSource;
}

public Object getWriteDataSource() {
return writeDataSource;
}

public Object getReadDataSource() {
return readDataSource;
}

public void setReadDataSource(Object readDataSource) {
this.readDataSource = readDataSource;
}
}
復(fù)制代碼
/**
* Created by IDEA
* User: mashaohua
* Date: 2016-07-14 10:58
* Desc:
*/

public enum DynamicDataSourceGlobal {
READ, WRITE;
}
復(fù)制代碼
public final class DynamicDataSourceHolder {

private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>();

private DynamicDataSourceHolder() {
//
}

public static void putDataSource(DynamicDataSourceGlobal dataSource){
holder.set(dataSource);
}

public static DynamicDataSourceGlobal getDataSource(){
return holder.get();
}

public static void clearDataSource() {
holder.remove();
}

}
復(fù)制代碼
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;

/**
* Created by IDEA
* User: mashaohua
* Date: 2016-08-10 14:34
* Desc:
*/

public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {

/**
* 只讀事務(wù)到讀庫,讀寫事務(wù)到寫庫
* @param transaction
* @param definition
*/

@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {

//設(shè)置數(shù)據(jù)源
boolean readOnly = definition.isReadOnly();
if(readOnly) {
DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ);
} else {
DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE);
}
super.doBegin(transaction, definition);
}

/**
* 清理本地線程的數(shù)據(jù)源
* @param transaction
*/

@Override
protected void doCleanupAfterCompletion(Object transaction) {
super.doCleanupAfterCompletion(transaction);
DynamicDataSourceHolder.clearDataSource();
}
}
復(fù)制代碼
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
* Created by IDEA
* User: mashaohua
* Date: 2016-08-10 11:09
* Desc:
*/

@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }) })

public class DynamicPlugin implements Interceptor {

protected static final Logger logger = LoggerFactory.getLogger(DynamicPlugin.class);

private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";

private static final Map<String, DynamicDataSourceGlobal> cacheMap = new ConcurrentHashMap<>();

@Override
public Object intercept(Invocation invocation) throws Throwable {

boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if(!synchronizationActive) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];

DynamicDataSourceGlobal dynamicDataSourceGlobal = null;

if((dynamicDataSourceGlobal = cacheMap.get(ms.getId())) == null) {
//讀方法
if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
//!selectKey 為自增id查詢主鍵(SELECT LAST_INSERT_ID() )方法,使用主庫
if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
} else {
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
if(sql.matches(REGEX)) {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
} else {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ;
}
}
}else{
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
}
logger.warn("設(shè)置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), dynamicDataSourceGlobal.name(), ms.getSqlCommandType().name());
cacheMap.put(ms.getId(), dynamicDataSourceGlobal);
}
DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal);
}

return invocation.proceed();
}

@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}

@Override
public void setProperties(Properties properties) {
//
}
}


作者:shawntime
鏈接:https://juejin.cn/post/6980233425361829924
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。



瀏覽 44
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報
評論
圖片
表情
推薦
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 亚洲AV成人精品日韩在线播放| 中文字幕日韩无码电影| 日本三级网| 夜夜撸网站| 成人视频18+在线观看| AV国产高清| 大香蕉第一页| 亚洲性爱专区| 伊人网视频在线观看| 成人av免费在线观看| 欧美一区二区三区免费| 欧美日韩人妻| 成人午夜天堂| 波多野结衣福利视频| 88av在线观看| 午夜乱伦| 欧美色图第一页| 亚洲在线无码视频| 国产精品a久久久久| 免费A在线观看| 一区二区三区www污污污网站| 污污污污污www在线观看优势| 久久久亚洲AV无码精品色午夜| 国产AV激情| 一本道高清无码视频| 夜夜夜操| 69婷婷国产精品| 欧美性爱视频免费观看| 色噜噜人妻av中文字幕| av无码av天天av天天爽| 亚洲精品视频在线观看免费| 日本久久婷婷| 欧美性爱香蕉视频| 日韩大香蕉| 国产乱伦免费| 你懂的在线播放| 男女啪啪动态图| 熟睡侵犯の奶水授乳在线| 天天躁夜夜躁狠狠躁AV| 久操超碰| 女女久久| 男女一区二区三区| 亚洲男女内射| 天天日天天干天天爽| 伊人蕉| 国产熟女一区二区久久| 漂亮人妻吃鸡啪啪哥哥真的好| 国产黄色视频在线| 国产午夜视频在线观看| 囯产精品久久久久久久久久| 久久77| 日韩综合另类| 久久AV秘一区二区三区水生| 超碰人人91| 日韩成人区| 久久永久免费视频| 欧美区亚洲区| 四虎日韩| 婷婷五月成人| 日韩欧美一区二区在线观看| 97人人爽人人爽人人爽人人爽| 狠狠色噜噜狠狠狠888| 亚洲激情欧美激情| 第四色色综合| 久草大香蕉在线视频| 亚洲欧美国产另类| 国产在线一二三| 超碰天天干天天摸| 国产一级婬片A片免费妖精视频| 亚洲无码门| 东京热无码视频| 视频一区在线观看| 久久久性爱| 大香蕉久| 激情六月| 中文字幕在线免费| 91成人免费视频| 91视频亚洲| 日本免费一区二区三区| 亚洲免费无码视频| 亚洲精品久久久久中文字幕二区| 毛片网页| 99在线视频观看| 国产黄色录像| 日韩性爱视频| 午夜撸一撸| 肉片无遮挡一区二区三区免费观看视频 | 玖玖激情| 青草青草视频| 逼逼网| 中文在线字幕免费观看| 午夜无码高清| 日本在线视频一区二区| 先锋影音麻豆| 少妇bbw搡bbbb搡bbbb| 综合色播| 最好看的MV中文字幕国语| 天天操夜夜操狠狠操| 嫩BBB搡BBB槡BBB小号| 国产区av| 乱伦性爱视频| 精品在线播放| 日韩综合网| 日韩高清国产一区在线| 日韩人妻无码一区二区三区中文| 久久久久久无码日韩欧美电影| 69成人导航| 亚洲无码操逼视频| 国产婷婷五月天| 3D精品啪啪一区二区三区| 国产91嫩草乱婬A片2蜜臀| 在线乱视频| 亚洲一区2区| av乱伦小说| 在线视频你懂| 日韩黄色在线视频| 911精品人妻一区二区三区A片| 一级一级一级做a免费一级做a| 爆草美女| 51午夜| 北条麻妃毛片| 中文字幕日韩成人| 日韩精品人妻中文字幕有| 日本天天色| 伊人三级片| AV片免费看| 欧美国产综合在线| 成人免费视频国产在线观看| 影音先锋色先锋| 色秘乱码一区二区三区唱戏| 激情小说激情视频| 狠狠操在线| 五月天婷婷丁香| 中文字幕亚洲在线| 精品国产乱码一区二区| 亚洲视频在线观看播放| 精品一区二区三区四区学生| 日无码视频| 日韩精品免费在线观看| 日韩家庭乱伦| 午夜成人黄片| 熟女无码| 欧美www| 日韩精品成人在线视频| 色操逼网| 国产中文字幕av| 在线国产中文字幕| 久久婷婷六月综合| 四虎884| 色逼视频| 丁香五月少妇| 人人妻人人做| 日韩成人AV电影| 色悠悠国产| 久久久久久久免费| 草久视频| 18XXX亚洲HD护士JD| 日韩大香蕉| 北条麻妃精品青青久久价格| 成人毛片视频网站| 肏屄视频网| 国产免费高清视频| 91绿帽人妻-ThePorn| 亚洲免费观看高清| 91人妻一区二区| 另类综合激情| 夜夜嗨AV| 青草青在线视频| 人人色人人干| 中文字幕视频一区| 国产乱伦精品视频| 真人一级片| 精品一区二区三区免费| 亚洲精品成人av无码| 91豆花视频| 中文字幕激情精品| 成人一区二区在线| 1插菊花网| 免费AV毛片| AV无码网站| 国产日韩欧美| 亚洲欧美日韩不卡| 青青草黄色视频| a级网站| 青娱乐无码| www.黄片| 91久久久久久久91| 91影音先锋| 亚洲中文字幕不卡| 欧美日韩一区视频| 亚洲三级黄色视频| 狠狠撸狠狠干| 欧美黄色激情视频网站| 国产91高跟丝袜| 日韩啪啪啪网站| 国产精品成人影视| 亚洲国产一区二区三区| 伊人网站视频在线| 婷婷久热| 国产精品乱子伦一区二区三区视频| 天天干天天日天天| 东京热视频在线观看| 玖玖热在线视频| 欧美国产在线观看| 波多野结衣一区二区| 怡红影院美乳| 日韩精品一级| 国产精品毛片A√一区| 日日骚亚洲| 大香蕉天天操| 色丁香五月婷婷| 成人三区| 久久精品国产AV一区二区三区| 九色PORNY丨自拍蝌蚪| 亚洲区成人777777精品| 最新一区二区三区| 欧美一区三区| 51国产黑料吃瓜在线入口| 久久综合热| 久久三级片| 高清无码高潮| 久久偷拍网| 操逼影片| 人妻精品一二三| 国产黄色片在线免费观看| 人人草在线观看| 日韩免费无码| 91白浆肆意四溢456| 人人天天久久| 无码窝在线观看| 精品国产乱子伦一区二区三区最新章| 成人视频高清无码| 亚洲AV无码成人精品区在线欢看| 狠狠草狠狠干| 亚洲vs无码秘蜜桃少妇小说| 肏屄综合网| 特级西西444www大精品| 婷婷国产亚洲精品网站| 日韩在线观看视频免费| 日韩综合在线观看| 在线免费观看AV片| 精品国产免费观看久久久_久久天天 | 婷婷爱要操| av亚洲波多野结衣白嫩水多波| 翔田千里無碼破解| 亚洲在线免费| 日B视频网站| 青草超碰| 91工厂露脸熟女| 91麻豆精品在线| 精品一区二区三区四区五区六区| 亚洲日韩Av无码中文字幕美国| 长泽梓黑人初解禁BDD07| 另类TS人妖一区二区三区| 91亚洲精品久久久久久久久久久久 | 青青娱乐亚洲无| 中文字幕巨肉乱码中文乱码| 国产毛片基地| 综合成人在线| 91搞鸡| 91亚洲精品在线| 无码三级AV| 欧美后门菊门交3p、| 影音先锋国产资源| 麻豆91免费看| 久草国产在线视频| 日日夜夜av| 青青娱乐亚洲无| 日韩在线一区二区三区四区| h片免费观看| 日韩在线视频网| 久久熟女| 97久久精品国产熟妇高清网| v天堂在线观看| 日韩人妻av| 99视频免费看| 亚洲av黄片| 人人爽人人爱| 欧美一级黄色性爱视频| 成人伊人综合网| www.色五月| 国模一区二区三区| 日啪| 天天精品| 免费观看亚洲视频| 日韩精品三级片| 国产精品久久久久久久久久二区三区| 大香蕉天天操| 亚洲久操| 亚州不卡| 韩日无码视频| 夜夜嗨av无码一区二区三区| 成人aV无码精品国产一区二区| 国产精品久久久久野外| 福利导航页| 亚洲AV秘无码苍井空| 俺来也官网欧美久久精品| 日韩性爱片| 黄色三级在线观看| 一级免费爱爱视频| 一级黄片免费观看| 久久成人三级| 婷婷丁香六月天| 黄色成人在线观看| 黄色一级片在线| 撸一撸在线视频| 东京热网站在线观看| 欧美精品一卡| 北条麻妃久久视频在线播放| 久久午夜无码鲁丝片主演是谁| 一区二区三区四区五区在线| 少妇成人网| 中文字幕人妻互换av久久| 日韩免费Av| 一区二区无码高清| 狼友视频报放| 欧美国产综合| 久久高清免费视频| 国产成人主播| 青娱乐自拍视频| 999精品| 97色在线| 最美人妖系列国产Ts涵涵| 拍真实国产伦偷精品| 国色天香一区二区| 中文字幕成人A片| 天堂亚洲AV无码精品成人| 人妻少妇精品无码| 51福利视频| 九九九九综合| 久久精品福利视频| 日韩三级片在线播放| 黄色录像一级片| 国产精品伊人| 日韩中文字幕久久| 91三级片在线播放| 另类老妇videos另类| 苍井空在线播放| 成人免看一级a一片A片| 中文字幕日韩无码片| av在线资源观看| 五月六月丁香激情视频| 安微妇搡BBBB搡BBBB| 亚洲熟女一区二区三区妖精| 国产乱码一区二区三区的区别| 国产A级毛片| 日韩美女免费视频| 久久九九99| 天天躁天干天干| 无码人妻一区二区三区免费九色| 欧美mv日韩mv国产| 亚洲无码免费看| 中文字幕AV一区| 99欧美| 久久国产免费视频| 欧美日韩在线免费观看| 婷婷五月国产| 日韩精品在线免费观看| 亚洲中文幕| 久久偷拍视频| 99精品全国免费观看| 欧美性交一区二区| 青娱乐成人在线| 婷婷深爱五月丁香网| 在线无码不卡| 亚洲情在线| 日韩经典无码| 午夜无码久久| 俺也去av| 国产av日韩av| 色五月在线| h片免费网站| 色老板免费视频| 黄色片a片| 色欲影视插综合一区二区三区| 久久久精品午夜人成欧洲亚洲韩国| 久久伊人中文字幕| 日韩欧美国产精品| 日韩一区二区三区在线视频| 亚洲国产精品久久| 成人免费视频网站| 玉米地一级婬片A片| 国产精品久久毛片| 中文字幕免费观看| 欧美拍拍| 欧美精品无码久久久精品酒店| 少妇白洁在线观看| 五月天福利网| 国产欧美一区二区三区在线看蜜臀| 伊人成人免费视频| 国产黄色视频在线| 人人插人人射| 中文字幕免| 日韩经典无码| 91欧美精品成人AAA片| 人妻熟女字幕一区二区| 91AV在线免费观看| 欧美三级片网站| 91AV免费观看| 大香蕉操B| 99伊人| 91国产精品在线| 久久一级片| 97天天干| 日韩AV无码专区亚洲AV紧身裤| 国产精品无码AV| 成人在线无码视频| 成人久久久| 日韩精品在线观看免费| 操美女嫩逼| 在线免费观看AV片| 亚洲天堂成人在线| 超碰免费在线| 色婷婷AV一区二区三区之e本道 | 精品无码一区二区三区蜜桃李宗瑞 | 亚洲五月六月| 黄色小视频免费看| 三浦恵子一级婬片A片| 综合欧美国产视频二区| 人人爽人人| 黄色视频在线观看| 国产九色91回来了| 蜜臀99久久精品久久久懂爱| 秋霞网一区二区| 国产婷婷久久| 少妇高潮在线| 久久激情国产| 亚洲三级片视频| 成人五区| 俺来也官网欧美久久精品| 99精品视频免费| 精品一区二区免费视频| 久久久久久国产| 欧美一区二区三区成人片在线| 欧美成人精品一级| 特级西西WWW888| 久操免费观看| 免费一级欧美片在线观看| 无码AV一区二区| 久色天堂| xxx久久| 亚洲秘无码一区二区三区,| 最近最经典中文MV字幕| 久久91人妻无码精品蜜桃HD| 国产成人精品一区二区三区| 日本爱爱免费播放视频| 夜夜爽日日爽| 超碰天天干| 亚洲AV无码成人精品区在线欢看| 艹b视频在线观看| 亚洲免费在线视频观看| 日韩三级网| 99久久婷婷国产综合精品青牛牛 | 97人人爱| 国产粉嫩在线观看| 91在线免费看| 大肉大捧一进一出两腿| 97色色五月天| 久久久精品午夜人成欧洲亚洲韩国 | 国产精品扒开腿做爽爽爽A片唱戏 中文字幕一区二区三区精华液 | 色婷婷激情视频| 怡红院爽妇网| 国产福利在线播放| 日本大香蕉伊人| 高清免费在线中文Av| 一区二区无码视频| 丁香五月婷婷六月| 日本黄色电影网站| 黄色免费看视频| 国产性播放| a片在线观看免费| 欧美成人毛片AAAAAA| 一本一道伊人99久久综| 人妻电影亚洲av| 安徽妇搡BBBB搡BBBB| 一级a免一级a做免费线看内裤 | 中文一区二区| 爆乳尤物一区二区三区| 午夜免费无码| 麻豆国产成人AV一区二区三区| 手机免费AV| 亚洲成色A片77777在线小说| 一道本无码视频| 中文字幕免费视频在线播放| 东京热一区二区三区| 亚洲自拍中文字幕| 国产高潮视频在线观看| 欧美爱爱网| 亚洲中文字幕影院| 亚洲一区久久| 四色婷婷| 黄网站免费观看| 女人操逼| 三级网站视频| 日韩av小说| 日韩成人片| 欧美午夜网站| 久久黄色视频免费观看| 久久青青草在线视频| 亚洲美女喷水视频| 一本道中文字幕| 天天操天天射天天日| 久久伊人亚洲| 免费中文字幕| 亚洲成人视频网站| 久久女女| 欧美群交videotv群交| 亚洲日韩欧美国产| 伊人毛片| 五月丁香大香蕉| 久久女人网| 亚洲无码人妻在线| 亚洲色777| 午夜社区| 性爱无码网站| 亚洲无码资源| 日本中文无码| 男人午夜AV| 爱操视频| 91亚洲精品乱码久久久久久蜜桃| 青草一区| 色色色免费视频| 日日射人妻| 五月天最新网址| 国产精品98| 日本亲子乱婬一级A片| 久操B网| 人人看人人摸| 搡BBBB搡BBB搡我瞎了| 国产嫩草精品A88AV| 亚洲AV成人无码一区二区三区 | 加勒比日韩无码| 黄色日逼视频| 亚洲一页| 无码人妻精品一区二区三区蜜桃91| 无码不卡中文字幕| 人人干AV| 无码一区二区北条| 天堂成人| 97在线观看免费视频| 内射| 强伦人妻一区二区三区视频| 人妻少妇中文字幕久久牛牛| 欧美九九| 苍井空一区二区| 日本精品中文字幕| 国产AV播放| 一本一道波多野结衣潮喷视频| 青娱乐国产精品| 91麻豆免费视频网站| 国精品91无码一区二区三区在线| A片免费在线播放| 翔田千里91| 国产探花一区二区三区| 天天干天天日蜜臀色欲av| 国产高清视频| av无码高清| 国精产品一区一区三区四川| 一本久久精品一区二区| 99re国产视频| 国产高清AV在线| 黄色A一级| 国产一区二区做爱| 超碰狠狠操| 日韩成人在线免费观看| 日韩午夜av| 亚洲视频91| 色综合色综合色综合| 成年人黄色视频免费观看| 欧美在线成人网| 高清无码在线免费观看| 久久99深爱久久99精品| 色天堂网站| 亚洲黄色免费电影| 中文字幕乱码中文乱码91| 亚洲免费观看高清完| 天天操大香蕉| 国产精品无码永久免费A片| 一区二区三区四区在线| 国产一级AAAAA片免费| 午夜神马福利| 国产精品在线观看视频| 丁香五月婷婷啪啪| 又色又爽| 一区二区三区无码专区| A∨无码| AV超碰| 在线观看国产区| 青青草无码成人AV片| 中文字幕在线观看视频www| 强辱丰满人妻HD中文字幕| 少妇白洁在线观看| 性生活黄色视频| av在线资源播放| 日本A片在线免费观看| 国产农村乱婬片A片AAA图片| 大香蕉伊人手机在线| 狠狠操一区| 99re欧美激情| 六月婷| 亚洲高清在线视频| 日韩A片在线观看| 人人摸人人操人人| 西西西444www无码视| 亚欧成人| 亚洲欧美日韩色图| 亚洲无码入口| 日韩无码视频免费| 91精品网| 黄色免费av| 欧美一级黃色A片免费看蜜桃熟了 一级a一级a免费观看免免黄‘/ | 国产二区三区| 999国产精品| 不卡不在线中文| 免费A在线观看| h片在线免费观看视频| 天堂在线8| 中文字幕日本成人| 大香蕉色视频| 亚洲欧美日韩成人| 免费看A片视频| 日韩激情视频在线观看| 囯产伦精一区二区三区四区| 第四色网站| 激情无码网站| 国产婷婷色一区二区| 免费看日P视频| 人妻无码视频| 91青青草| 西西444WWW大胆无| av高清无码| 欧美成人无码片免费看A片秀色 | 91人妻人人爽人人澡| 亚洲毛片网站| 在线视频福利导航| 欧美黄色成人视频| 狠狠噜噜| 操极品少妇逼| 黄片网站在线免费观看| 国产人成| 男人天堂无码视频| 亚洲精品成人网站| 五月丁香激情在线| 亚洲AV无码乱码国产| 在线观看小视频| 美女高潮在线| 日韩在线二区| 福利导航视频| 69国产精品视频免费观看| 美日韩AV| 日韩欧美国产综合| 澳门午夜| 午夜福利播放| 欧美黄页| 亚洲欧美在线免费观看| 天堂一区在线观看| 亚洲成人影音| 日韩人妻AV| 国产三级片自拍| 台湾一区二区| 巜痴漢電車~凌脔版2| 日本在线精品视频| 北条麻妃99| 十八禁免费网站| 色二区| 中文字幕人妻日韩在线| 日韩免费一级片| 黄色小电影在线观看| 97国产视频| 色情欧美一级A片| 亚洲精品乱码久久久久久按摩观| 欧美黄色激情视频网站| 啪视频网站国产馆| 日屄免费视频| 国产小毛片| 97精品人妻一区二区三区香蕉农 | 国产视频导航| 超碰人人操| 麻豆免费福利视频| AV狠狠干| 日韩在线观看视频免费| 91精品国产综合久久久蜜臀粉嫩| 中文字幕在线观看网| 亚洲成人一二三区| 性爱视频网页| 国产视频一二三| 四川BBBBBB搡BBBBB| 午夜精东影业传媒在线观看| 成人无码日韩| 女生自慰网站在线观看| 亚洲性爱av| 久久久久久网| 福利一区二区| 久久久久久无码日韩欧美电影| 麻豆成人无码| 一级片AA| 国产美女在线观看| 免费在线观看A| 国产黄色小视频在线观看| 大地资源38页| 91资源超碰| 操逼在线免费观看| 中文字幕成人网| www.| 亚洲二区后入极品| 亚洲第一色在线| 男人资源在线| 无码人妻一区二区三一区免费n狂飙 | 黄色免费看| 亚洲无码电影网站| 五月婷婷中文| 欧美在线中文字幕| 丝袜人妻被操视频| 国产真人一级a爱做片| 思思精品视频| 黄色视频免费观看国产| 3d啪啪动漫| 在线观看亚洲专区| 一区二区视频在线| 久久无码一区二区三区| 六月色| 最新版本日本亚洲色| 俺也去俺去啦| 老熟女网站| 国内精品久久久久久久久久变脸| 要操逼网| 波多野结衣高清无码视频| 人人操人人干人人摸| 亚洲va| 中文字幕av高清片,中文在线观看| 中文在线最新版天堂8| 日韩黄色免费网站| 欧美性成人| 久热在线精品视频| 欧美亚洲中文| 尻屄网站| 逼特逼视频| 水多多成人网站A片| 毛片小电影| 成人亚洲av| 一区二区无码区| 亚洲黄视频| 91精品久久久久久久| AV天堂影视在线观看| 国产高清精品无码| 天堂视频在线观看亚洲美女 | 亚洲无码99| 亚洲WWW| 亚洲女与黑人正在播放| 亚洲视频在线观看播放| 欧美亚洲精品在线| 欧美后门菊门交3p| 日韩三级在线观看| 无码人妻视频| 亚洲精品欧美久久婷婷| 91AV久久| 可以看的毛片| 九九福利| 色婷婷一区二区| 爱爱打炮影院| 1024国产在线| 亚洲精品无码更新| 亚洲天堂手机在线| 国产主播在线播放| 一级片日韩| 日韩大片免费观看| 亚洲男女内射| 老太色HD色老太HD-百度| 性饥渴熟妇乱子伦| 99视频内射三四| 久久人妻免费视频| 亚洲欧美视频| 色爱av| 久操人妻| 啪啪免费视频| 成人国产片女人爽到高潮| 日本中文字幕精品| 无码精品人妻| 国产91福利| 一本在线| 一道本无码免费视频| 国产三级片91| 国产亚洲欧美精品综合在线 | sm国产在线调教视频| 麻豆AV无码| 小處女末发育嫩苞AV| 日批视频| 久久综合伊人7777777| 亚洲伊人大香蕉| 欧美日韩第一页| 91搞搞| 国产不卡精品| 91精品婷婷国产综合| 一区二区三区免费在线| 国产精品无码永久免费不卡 | 亚洲日韩一区| 五月丁香无码| 日本久久综合网| 欧美日韩在线视频播放| 五月天操逼网| 国产激情123区| 337P粉嫩大胆噜噜噜55569| 国内成人AV| 九九热精品在线| 日韩精彩视频| 亚洲一区翔田千里无码| 蜜桃AV在线播放| 97超碰碰| WW免费视频| 国产丝袜自拍| 日韩色区| AV一区二区三区| 人妻免费在线视频| 亚洲婷婷网| 欧美视频二区| 99精品六月婷婷综合在线| AV国产高清| 欧美VA视频| 日韩中文字幕AV| 浮力影院久久| 2018中文字幕第一页| 免费黄色三级片| 成人黄色免费| 四川少妇搡BBw搡BBBB搡| 国产粉嫩在线观看| 国产精品国产| 老司机狠狠干| 台湾精品一区二区三区| 日韩人妻斩| 五月婷婷丁香五月| 熟妇高潮一区二区高潮| 好男人av| 东北女人操逼| 艹逼电影| 手机在线操B视频| 学生妹作爱片| 91理伦| 超碰碰碰碰碰| 免费黄片在线看| 一级a一级a爰片免费免免在线| 老太色HD色老太HD-百度| 欧美乱码| 久久久久国产一区二区三区四区| 天天草视频| 亚洲欧洲成人在线| 久草福利| 免费无码国产在线| 簧片网站在线观看| 99中文字幕| www九九| 北条麻妃人妻中文字幕91影视 | 国产1区2区3区中文字幕| 亚洲精品无码久久久| 人人妻人人澡人人爽久久con | 男人的天堂视频在线| 亚洲AV无码精品成人| 先锋AV资源| 国产福利在线导航| 群交无码| 大香蕉在线观看视频| 成人天天爽| 亚洲美女免费视频| 97人人艹| 俺去也俺去啦| 国产精品a久久久久| 成人国产精品秘在线看| 欧洲综合视频| 91九色91蝌蚪91窝成人| 青草社区在线观看| 婷婷六月色| 黄色小视频免费观看| 91麻豆国产在线观看| 天天搞天天干| 最好看的MV中文字幕国语| 日本一级理论片在线大全| 韩日在线视频| 一区二区毛片| 91N视频| 天天操天天射天天日| 中文字幕有码在线| 怡红院成人在线| 做爱视频无码| 日本一级做a爱片| 久久久77| 国产艹逼视频| 中文字幕无码毛片| 日韩久久人妻| 天天久久综合| 精品国产香蕉| 亚洲无码精品久久| 青娱乐偷窥成| 色福利视频| 欧美青青草| 欧美怡红院视频| 男人天堂色| 看操b视频| 日本精品在线播放| 毛片网站在线| 亚洲Av在线观看| 久久久久久综合|