1. 程式人生 > >多資料庫,多資料來源,資料庫路由

多資料庫,多資料來源,資料庫路由

                                首先,要明白為什麼要使用多資料庫?

          正常情況下,如果,百萬資料在單個mysql其實沒太大影響的,但是如果資料達到了,千萬,億,甚至更多,從資料庫著手我們得對資料庫進行拆分成多個庫,但是多個庫之間的資料操作,這就涉及到了我們的資料來源之間的路由的,但是具體程式碼怎麼實現的呢?這裡我用spring+springmvc+hibernate+maven做成案例,廢話不多說,直接上程式碼。

專案結構圖

資料庫

pom.xml引入的jar包

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.lcj.blog</groupId>
	<artifactId>datasourceDynamic</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<spring.version>4.2.6.RELEASE</spring.version>
	</properties>

	<dependencies>
		<!-- springframework 4 dependencies begin -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<!-- springframework 4 dependencies end -->

		<!-- hibernate 配置 begin -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>3.6.9.Final</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>3.6.9.Final</version>
		</dependency>
		<!-- hibernate 配置 end -->

		<!-- mysql資料庫的驅動包 start -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.6</version>
		</dependency>

		<!-- 引入jstl包 -->
		<dependency>
			<groupId>javax.servlet.jsp.jstl</groupId>
			<artifactId>jstl-api</artifactId>
			<version>1.2</version>
		</dependency>

		<!-- 引入servlet -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
		</dependency>

		<!-- 引入資料庫連線池 -->
		<dependency>
			<groupId>commons-dbcp</groupId>
			<artifactId>commons-dbcp</artifactId>
			<version>1.4</version>
		</dependency>
		<dependency>
			<groupId>commons-pool</groupId>
			<artifactId>commons-pool</artifactId>
			<version>1.6</version>
		</dependency>

		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.8.6</version>
		</dependency>
	</dependencies>
</project>
!

spring.xml配置,主要的是多資料來源初始化,sessionFactory工廠,事物,切面

<?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:aop="http://www.springframework.org/schema/aop"
	xmlns:cache="http://www.springframework.org/schema/cache"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:jms="http://www.springframework.org/schema/jms" xmlns:lang="http://www.springframework.org/schema/lang"
	xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:oxm="http://www.springframework.org/schema/oxm"
	xmlns:p="http://www.springframework.org/schema/p" xmlns:task="http://www.springframework.org/schema/task"
	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd 
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd  
   	http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.3.xsd  
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd  
    http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd  
    http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.3.xsd  
    http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-4.3.xsd  
  	http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-4.3.xsd  
   	http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd  
  	http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-4.3.xsd  
 	http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd  
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd  
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd">

	<!-- 資料來源,BasicDataSource,commons-dbcp -->
	<bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url"
			value="jdbc:mysql://localhost:3306/dataSource1?useUnicode=true&amp;characterEncoding=UTF-8" />
		<property name="username" value="root" />
		<property name="password" value="123456" />
	</bean>

	<bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url"
			value="jdbc:mysql://localhost:3306/dataSource2?useUnicode=true&amp;characterEncoding=UTF-8" />
		<property name="username" value="root" />
		<property name="password" value="123456" />
	</bean>

	<!-- 配置資料來源 -->
	<bean id="dataSource" class="com.liu.core.datasource.DynamicDatasource">
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry key="dataSource1" value-ref="dataSource1" />
				<entry key="dataSource2" value-ref="dataSource2" />
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="dataSource1" />
	</bean>

	<!-- Hibernate SesssionFactory -->
	<bean id="sessionFactory"
		class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="hibernateProperties">
			<props>
				<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
				<prop key="hibernate.format_sql">true</prop>
				<prop key="hibernate.hbm2ddl.auto">update</prop>
			</props>
		</property>
		<property name="mappingLocations" value="classpath:/com/liu/model/*.hbm.xml" />
	</bean>

	<!-- 事物模板 -->
	<bean id="transactionManager"
		class="org.springframework.orm.hibernate3.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory" />
	</bean>
 
	<bean id="transactionTemplate"
		class="org.springframework.transaction.support.TransactionTemplate">
		<property name="transactionManager">
			<ref bean="transactionManager" />
		</property>
	</bean>

 
	<!-- 配置資料來源切換切面 引數配置 -->
	<bean id="dataSourceChangeAspect" class="com.liu.core.datasource.DatasourceAspect">
		<property name="defaultDatasource" value="dataSource1"></property>
		<property name="datasourceMap">
			<map key-type="java.lang.String">
				<!-- 通過不同的key決定用哪個dataSource -->
				<entry key="com.liu.dao.jdbc.UserDaoImpl" value="dataSource1"></entry>
				<entry key="com.liu.dao.jdbc.BookDaoImpl" value="dataSource2"></entry>
			</map>
		</property>
 
	</bean>
	<!--在執行事務前必須切換好事據源,注意執行順序 order越小執行越早 -->
	<!-- 切面aop -->
	<!-- 1、execution(): 表示式主體。 2、第一個*號:表示返回型別,*號表示所有的型別。 3、包名:表示需要攔截的包名,後面的兩個句點表示當前包和當前包的所有子包,com.sample.service.impl包、子孫包下所有類的方法。 
		4、第二個*號:表示類名,*號表示所有的類。 5、*(..):最後這個星號表示方法名,*號表示所有的方法,後面括弧裡面表示方法的引數,兩個句點表示任何引數。 -->
	<aop:config>
		<aop:aspect id="datasourceAspect" ref="dataSourceChangeAspect"
			order="0">
			<aop:pointcut id="daoAop" expression="execution(* com.liu.dao..*.*(..))" />
			<aop:before pointcut-ref="daoAop" method="tabDataSource" />
			<aop:after-returning pointcut-ref="daoAop"
				method="doAfterReturning" />
		</aop:aspect>
	</aop:config>
</beans>
 

springmvc.xml配置,主要是註解開啟,掃描包,檢視層字尾配置(要不要無所謂)

<?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:aop="http://www.springframework.org/schema/aop"
	xmlns:cache="http://www.springframework.org/schema/cache"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:jms="http://www.springframework.org/schema/jms" xmlns:lang="http://www.springframework.org/schema/lang"
	xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:oxm="http://www.springframework.org/schema/oxm"
	xmlns:p="http://www.springframework.org/schema/p" xmlns:task="http://www.springframework.org/schema/task"
	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd 
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd  
   	http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.3.xsd  
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd  
    http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd  
    http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.3.xsd  
    http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-4.3.xsd  
  	http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-4.3.xsd  
   	http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd  
  	http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-4.3.xsd  
 	http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd  
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd  
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd">
    
	<aop:aspectj-autoproxy />
	
	<!-- 註解支援 -->
	<context:annotation-config />
	
	<!-- 定義掃描根路徑為com,不使用預設的掃描方式 -->
	<context:component-scan base-package="com"></context:component-scan>

	<!-- 啟用基於註解的配置 @RequestMapping, @ExceptionHandler,資料繫結 ,@NumberFormat , 
		@DateTimeFormat ,@Controller ,@Valid ,@RequestBody ,@ResponseBody等 -->
	<mvc:annotation-driven />

	<!-- 檢視層配置 -->
	<bean
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<!--配置JSTL表示式 -->
		<property name="viewClass"
			value="org.springframework.web.servlet.view.JstlView" />
		<!-- 字首 -->
		<property name="prefix" value="/WEB-INF/jsp/" />
		<!-- 字尾 -->
		<property name="suffix" value=".jsp" />
	</bean>
</beans>  
 

web.xml,spring容器,轉發器,監聽器等

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	id="WebApp_ID" version="3.0">
	<display-name>datasourceDynamic</display-name>
	<welcome-file-list>
		<welcome-file>index.do</welcome-file>
	</welcome-file-list>
	<!-- 配置spring容器 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring.xml</param-value>
	</context-param>

	<!-- 配置監聽器 -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- 請求都交給DispatcherServlet處理 -->
	<servlet>
		<servlet-name>springmvc</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:spring-mvc.xml</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>springmvc</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

	<!-- 增加中文亂碼過濾器 -->
	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<!-- 清除jsp空格 -->
	<jsp-config>
		<jsp-property-group>
			<url-pattern>*.jsp</url-pattern>
			<trim-directive-whitespaces>true</trim-directive-whitespaces>
		</jsp-property-group>
	</jsp-config>
</web-app>
 

最後我放幾個關鍵的類,其餘dao,service,controller就不放,意義沒有太大,理解原理就行了

DatasourceAspect動態資料來源切面

/**
 * 
 */
package com.liu.core.datasource;

import java.util.Map;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;

 
/**
 * 動態資料來源切面
 * 
 * @author liuchaojun
 * @date 2018-12-12 上午10:42:11
 */
@Component
@Aspect
public class DatasourceAspect {
	private Map<String, String> datasourceMap;
	private String defaultDatasource;
 

	/*
	 * 切入後執行的方法
	 */
	public void tabDataSource(JoinPoint joinPoint) {
		boolean flag = false;
		// 首先通過切點獲得類名
		String className = joinPoint.getTarget().getClass().getName();
		System.out.println(className);
		for (String datasourceKey : datasourceMap.keySet()) {
			if (datasourceKey.equals(className)) {
				// 初始化資料來源
				DatasourceTabUtils.setDatasourceName(datasourceMap
						.get(datasourceKey));
				System.out.println("切換資料來源...."+datasourceMap
						.get(datasourceKey));
				flag = true;
				break;
			}
		}
		if (!flag) {
			DatasourceTabUtils.setDatasourceName(defaultDatasource);
		}
	}

	/*
	 * 做完後的執行的方法 清除執行緒變數
	 */
	public void doAfterReturning(JoinPoint joinPoint) {
		DatasourceTabUtils.clear();
	}

	/**
	 * @return the datasourceMap
	 */
	public Map<String, String> getDatasourceMap() {
		return datasourceMap;
	}

	/**
	 * @param datasourceMap
	 *            the datasourceMap to set
	 */
	public void setDatasourceMap(Map<String, String> datasourceMap) {
		this.datasourceMap = datasourceMap;
	}

	/**
	 * @return the defaultDatasource
	 */
	public String getDefaultDatasource() {
		return defaultDatasource;
	}

	/**
	 * @param defaultDatasource
	 *            the defaultDatasource to set
	 */
	public void setDefaultDatasource(String defaultDatasource) {
		this.defaultDatasource = defaultDatasource;
	}
 
}

DatasourceTabUtils  資料來源切換工具類  本地執行緒變數操作

/**
 * 
 */
package com.liu.core.datasource;

/**
 * @author liuchaojun
 * @date 2018-12-12 上午10:43:47
 */
public class DatasourceTabUtils {
	private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

	public static void setDatasourceName(String datasourceName) {
		threadLocal.set(datasourceName);
	}

	public static Object getDatasourceName() {
		return threadLocal.get();
	}

	public static void clear() {
		threadLocal.remove();
	}
}

DynamicDatasource  動態資料來源路由類   這個類必須實現AbstractRoutingDataSource覆蓋determineCurrentLookupKey的方法

AbstractRoutingDataSource是Spring2.0後增加的。

這個其實也是資料來源切換的關鍵類,我們看下原始碼,簡單瞭解下

/*jadclipse*/// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) radix(10) lradix(10) 
// Source File Name:   AbstractRoutingDataSource.java

package org.springframework.jdbc.datasource.lookup;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.util.Assert;

// Referenced classes of package org.springframework.jdbc.datasource.lookup:
//            JndiDataSourceLookup, DataSourceLookup

public abstract class AbstractRoutingDataSource extends AbstractDataSource
    implements InitializingBean
{

    public AbstractRoutingDataSource()
    {
        lenientFallback = true;
        dataSourceLookup = new JndiDataSourceLookup();
    }

    public void setTargetDataSources(Map targetDataSources)
    {
        this.targetDataSources = targetDataSources;
    }

    public void setDefaultTargetDataSource(Object defaultTargetDataSource)
    {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    public void setLenientFallback(boolean lenientFallback)
    {
        this.lenientFallback = lenientFallback;
    }

    public void setDataSourceLookup(DataSourceLookup dataSourceLookup)
    {
        this.dataSourceLookup = ((DataSourceLookup) (dataSourceLookup == null ? ((DataSourceLookup) (new JndiDataSourceLookup())) : dataSourceLookup));
    }

    public void afterPropertiesSet()
    {
        if(targetDataSources == null)
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        resolvedDataSources = new HashMap(targetDataSources.size());
        Object lookupKey;
        DataSource dataSource;
        for(Iterator iterator = targetDataSources.entrySet().iterator(); iterator.hasNext(); resolvedDataSources.put(lookupKey, dataSource))
        {
            java.util.Map.Entry entry = (java.util.Map.Entry)iterator.next();
            lookupKey = resolveSpecifiedLookupKey(entry.getKey());
            dataSource = resolveSpecifiedDataSource(entry.getValue());
        }

        if(defaultTargetDataSource != null)
            resolvedDefaultDataSource = resolveSpecifiedDataSource(defaultTargetDataSource);
    }

    protected Object resolveSpecifiedLookupKey(Object lookupKey)
    {
        return lookupKey;
    }

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

    public Connection getConnection()
        throws SQLException
    {
        return determineTargetDataSource().getConnection();
    }

    public Connection getConnection(String username, String password)
        throws SQLException
    {
        return determineTargetDataSource().getConnection(username, password);
    }

    public Object unwrap(Class iface)
        throws SQLException
    {
        if(iface.isInstance(this))
            return this;
        else
            return determineTargetDataSource().unwrap(iface);
    }

    public boolean isWrapperFor(Class iface)
        throws SQLException
    {
        return iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface);
    }

    protected DataSource determineTargetDataSource()
    {
        Assert.notNull(resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = (DataSource)resolvedDataSources.get(lookupKey);
        if(dataSource == null && (lenientFallback || lookupKey == null))
            dataSource = resolvedDefaultDataSource;
        if(dataSource == null)
            throw new IllegalStateException((new StringBuilder()).append("Cannot determine target DataSource for lookup key [").append(lookupKey).append("]").toString());
        else
            return dataSource;
    }

    protected abstract Object determineCurrentLookupKey();

    private Map targetDataSources;
    private Object defaultTargetDataSource;
    private boolean lenientFallback;
    private DataSourceLookup dataSourceLookup;
    private Map resolvedDataSources;
    private DataSource resolvedDefaultDataSource;
}


/*
	DECOMPILATION REPORT

	Decompiled from: F:\mavenJarBaoLcj\.m2\org\springframework\spring-jdbc\4.2.6.RELEASE\spring-jdbc-4.2.6.RELEASE.jar
	Total time: 62 ms
	Jad reported messages/errors:
	Exit status: 0
	Caught exceptions:
*/

我們直接關注determineTargetDataSource方法的determineCurrentLookupKey的方法,我們通過整合這個抽象類實現這方法最後返回的是資料來源編號,也就相當於資料來源的key,spring會根據key來路由底層資料來源,而我們只需要提供具體切換的是哪一個,我們剛好從本地執行緒裡面去設定和拿取返回給這個determineCurrentLookupKey的方法。

其實就是相當於,把資料來源資料來源動態全部載入到程式中,通過aop靈活的進行資料來源切換,實現了讀寫分離,缺點無法動態的增加資料來源。

最後也說明一下,這個做法只支援單事物,也就是預設配置的事物,並且事物和資料來源切換初始化,資料來源一定要在前面,事物在後面一點,設定order越小越在前面載入。 

如果需要跨庫事物的切換或者分散式二階段事物,由於內容過多,我們在下一遍文章分開進行講解,事物也要根據具體專案業務哪種合適進行取捨。

本專案原始碼:連結:https://pan.baidu.com/s/1d4WBCnvheKK64MBHhUUdaw  提取碼:ddlv