在應對日漸複雜的業務環境,單個資料庫所能承載的壓力已經遠遠不夠。很多業務中誕生了主從資料庫的架構模型,將數據讀寫進行分離,主庫寫,從庫讀,以提升服務的吞吐量。 在進行代碼設計的時候,我們很自然會想到一個問題,一個業務操作,往往會包括讀 和 寫,例如在實現一個閱讀點擊量的簡單需求的時候,是不是需要先查 ...
在應對日漸複雜的業務環境,單個資料庫所能承載的壓力已經遠遠不夠。很多業務中誕生了主從資料庫的架構模型,將數據讀寫進行分離,主庫寫,從庫讀,以提升服務的吞吐量。
在進行代碼設計的時候,我們很自然會想到一個問題,一個業務操作,往往會包括讀 和 寫,例如在實現一個閱讀點擊量的簡單需求的時候,是不是需要先查詢一下原來有多少點擊量Num,然後再給這個獲取到的數據Num進行+1操作呢?
那麼問題來了:
如果很多人同時點擊,都在給這個Num進行+1,咱們姑且不論數據沒辦法實現真實穩定的問題。就單從主庫和從庫切換上來說,是不是已經會產生問題呢?
仔細分析一下,假如用戶A此時在調用介面讀取數據,而用戶B在調用介面進行寫。這個時候會有兩種情況出現,資料庫操作全局變數在經過A操作之後,變成了從庫,那麼這時候B寫的時候就會寫到從庫裡面了。第二種情況是,A還沒有完成讀取從庫的行為,B將全局變數設置為主庫,數據能夠正常寫入主庫,但是問題也出現了,此時A讀取數據的時候可能就是主庫了,讀寫分離的意義也就蕩然無存了。而線上實際情況,往往比這個複雜一點。在我經歷的項目中,讀寫數據的行為稍有不當,可能帶來的是項目的巨大損失。
回顧一下上篇文章:各掃門前雪的ThreadLocal
我提到過,ThreadLocal是線程安全的,藉助ThreadLocal,我們可以比較好的規避上面的這個問題。聰明的你一定馬上會想到,對了,把這個資料庫切換的全局變數,變成一個線程的“本地”變數,不就安全多了嗎?
下麵我們來簡單寫個實現類
主從庫切換類,基於ThreadLocal實現調用介面線程的安全性。
public class DynamicDataSource extends AbstractRoutingDataSource { private final static Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class); private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
@Override protected Object determineCurrentLookupKey() {
String dataSource = getDataSource();return dataSource; } /** * 設置數據源 * * @param dataSource */ public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource); } /** * 獲取數據源 * */ public static String getDataSource() {
String dataSource = CONTEXT_HOLDER.get();
// 如果沒有指定數據源,使用預設數據源 if (null == dataSource) { DynamicDataSource.setDataSource(DataSourceEnum.MASTER.getDefault()); } return CONTEXT_HOLDER.get(); } /** * 清除數據源 */ public static void clearDataSource() {
CONTEXT_HOLDER.remove(); } }
>> 當需要進行資料庫操作的時候,調用
DynamicDataSource.setDataSource(DataSourceEnum.MASTER.getName());
或者 DynamicDataSource.setDataSource(DataSourceEnum.SLAVE.getName());
切換資料庫;
>> 用完了,調用
DynamicDataSource.clearDataSource();
以便下一次繼續使用
其他關聯數據和配置:
主從庫枚舉:
public enum DataSourceEnum { // 主庫 MASTER("masterDataSource", true), // 從庫 SLAVE("slaveDataSource", false),; // 數據源名稱 private String name; // 是否是預設數據源 private boolean master; DataSourceEnum(String name, boolean master) { this.name = name; this.master = master; } public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isMaster() { return master; } public void setMaster(boolean master) { this.master = master; } public String getDefault() { String defaultDataSource = ""; for (DataSourceEnum dataSourceEnum : DataSourceEnum.values()) { if (!"".equals(defaultDataSource)) { break; } if (dataSourceEnum.master) { defaultDataSource = dataSourceEnum.getName(); } } return defaultDataSource; } }
主從庫的配置如下:
<!-- 主庫數據源 --> <bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 基本屬性 url、user、password --> <property name="driverClassName" value="${master.jdbc.driver}"/> <property name="url" value="${master.jdbc.url}"/> <property name="username" value="${master.jdbc.username}"/> <property name="password" value="${master.jdbc.password}"/> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="1"/> <property name="minIdle" value="1"/> <property name="maxActive" value="20"/> <!-- 配置獲取連接等待超時的時間 --> <property name="maxWait" value="60000"/> <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="300000"/> <!-- 校驗語句 --> <property name="validationQuery" value="SELECT 1"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <!-- 配置監控統計攔截的filters --> <property name="filters" value="stat"/> </bean> <!-- 從庫數據源 --> <bean id="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 基本屬性 url、user、password --> <property name="driverClassName" value="${slave.jdbc.driver}"/> <property name="url" value="${slave.jdbc.url}"/> <property name="username" value="${slave.jdbc.username}"/> <property name="password" value="${slave.jdbc.password}"/> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="1"/> <property name="minIdle" value="1"/> <property name="maxActive" value="20"/> <!-- 配置獲取連接等待超時的時間 --> <property name="maxWait" value="60000"/> <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="300000"/> <!-- 校驗語句 --> <property name="validationQuery" value="SELECT 1"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <!-- 配置監控統計攔截的filters --> <property name="filters" value="stat"/> </bean>
好了 ,代碼雖然看起來挺簡單的,但也能說明ThreadLocal在實現讀寫分離時候的有它一席用武之地咯。實際應用場景,會有更複雜的處理。
備註:此案例借用了網上的一些資源,做了簡化處理,只作為演示說明使用。