不少時候,爲了應付DB的高併發讀寫,咱們會採用讀寫分離技術。讀寫分離指的是利用數據庫主從技術(把數據複製到多個節點中),分散讀多個庫以支持高併發的讀,而寫只在master庫上。DB的主從技術只負責對數據進行復制和同步,而讀寫分離技術須要業務應用自身去實現。sharding-jdbc經過簡單的開發,能夠方便的實現讀寫分離技術。本篇主要介紹其實現的原理。java
sharding-jdbc官方對其支持的讀寫分離技術進行了說明:mysql
支持項
提供了一主多從的讀寫分離配置,可獨立使用,也可配合分庫分表使用。
同個調用線程,執行多條語句,其中一旦發現有非讀操做,後續全部讀操做均從主庫讀取。
Spring命名空間。
基於Hint的強制主庫路由。算法
不支持範圍
主庫和從庫的數據同步。
主庫和從庫的數據同步延遲致使的數據不一致。
主庫雙寫或多寫。spring
簡單說明
sharding-jdbc實現讀寫分離技術的思路比較簡潔,不支持相似主庫雙寫或多寫這樣的特性,但目前來看,已經能夠知足通常的業務需求了。sql
庫和表的設計結構以下:
數據庫
簡單的java代碼示例:併發
public final class MasterSlaveMain { public static void main(final String[] args) throws SQLException { DataSource dataSource = getShardingDataSource(); printSimpleSelect(dataSource); } private static void printSimpleSelect(final DataSource dataSource) throws SQLException { String sql = "SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.user_id=? AND o.order_id=?"; try ( Connection conn = dataSource.getConnection(); PreparedStatement preparedStatement = conn.prepareStatement(sql)) { preparedStatement.setInt(1, 10); preparedStatement.setInt(2, 1001); try (ResultSet rs = preparedStatement.executeQuery()) { while (rs.next()) { System.out.println(rs.getInt(1)); System.out.println(rs.getInt(2)); System.out.println(rs.getInt(3)); } } } } private static ShardingDataSource getShardingDataSource() throws SQLException { DataSourceRule dataSourceRule = new DataSourceRule(createDataSourceMap()); TableRule orderTableRule = TableRule.builder("t_order").actualTables(Arrays.asList("t_order_0", "t_order_1")).dataSourceRule(dataSourceRule).build(); TableRule orderItemTableRule = TableRule.builder("t_order_item").actualTables(Arrays.asList("t_order_item_0", "t_order_item_1")).dataSourceRule(dataSourceRule).build(); ShardingRule shardingRule = ShardingRule.builder().dataSourceRule(dataSourceRule).tableRules(Arrays.asList(orderTableRule, orderItemTableRule)) .databaseShardingStrategy(new DatabaseShardingStrategy("user_id", new ModuloDatabaseShardingAlgorithm())) .tableShardingStrategy(new TableShardingStrategy("order_id", new ModuloTableShardingAlgorithm())).build(); return new ShardingDataSource(shardingRule); } private static Map<String, DataSource> createDataSourceMap() throws SQLException { Map<String, DataSource> result = new HashMap<>(2, 1); Map<String, DataSource> slaveDataSourceMap1 = new HashMap<>(2, 1); slaveDataSourceMap1.put("ds_0_slave_0", createDataSource("ds_0_slave_0")); slaveDataSourceMap1.put("ds_0_slave_1", createDataSource("ds_0_slave_1")); result.put("ds_0", MasterSlaveDataSourceFactory.createDataSource("ds_0", "ds_0_master", createDataSource("ds_0_master"), slaveDataSourceMap1)); Map<String, DataSource> slaveDataSourceMap2 = new HashMap<>(2, 1); slaveDataSourceMap2.put("ds_1_slave_0", createDataSource("ds_1_slave_0")); slaveDataSourceMap2.put("ds_1_slave_1", createDataSource("ds_1_slave_1")); result.put("ds_1", MasterSlaveDataSourceFactory.createDataSource("ds_1", "ds_1_master", createDataSource("ds_1_master"), slaveDataSourceMap2)); return result; } private static DataSource createDataSource(final String dataSourceName) { BasicDataSource result = new BasicDataSource(); result.setDriverClassName(com.mysql.jdbc.Driver.class.getName()); result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName)); result.setUsername("root"); result.setPassword("123456"); return result; } }
private static DataSource createDataSource(final String dataSourceName) { BasicDataSource result = new BasicDataSource(); result.setDriverClassName(com.mysql.jdbc.Driver.class.getName()); result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName)); result.setUsername("root"); result.setPassword("123456"); return result; } }
通常咱們是這樣來執行sql語句的:負載均衡
Connection conn = dataSource.getConnection(); PreparedStatement preparedStatement = conn.prepareStatement(sql); preparedStatement.executeQuery();
這是利用原生jdbc操做數據庫查詢語句的通常流程,獲取一個鏈接,而後生成Statement,最後再執行查詢。那麼sharding-jdbc是在哪一塊進行擴展從而實現讀寫分離的呢?框架
想一下,想要實現讀寫分離,必然會涉及到多個底層的Connection,從而構造出不一樣鏈接下的Statement語句,而不少第三方軟件,如Spring,爲了實現事務,調用dataSource.getConnection()以後,在一次請求過程當中,可能就不會再次調用getConnection方法了,因此在dataSource.getConnection中作讀寫擴展是不可取的。爲了更好的說明問題,看下面的例子:ide
Connection conn = getConnection(); PreparedStatement preparedStatement1 = conn.prepareStatement(sql1); preparedStatement1.executeQuery(); Connection conn2 = getConnection(); PreparedStatement preparedStatement2 = conn2.prepareStatement(sql2); preparedStatement2.executeUpdate();
一次請求過程當中,爲了實現事務,通常的作法是當線程第一次調用getConnection方法時,獲取一個底層鏈接,而後存儲到ThreadLocal變量中去,下次就直接在ThreadLocal中獲取了。爲了實現一個事務中,針對一個數據源,既可能獲取到主庫鏈接,也可能獲取到從庫鏈接,還可以切換,sharding-jdbc在PreparedStatement(實際上爲ShardingPreparedStatement)的executeXX層進行了主從庫的鏈接處理。
下圖爲sharding-jdbc執行的部分流程:
sharding-jdbc使用ShardingPreparedStatement來替代PreparedStatement,在執行ShardingPreparedStatement的executeXX方法時,經過路由計算,獲得PreparedStatementUnit單元列表,而後執行後合併結果返回,而PreparedStatementUnit只不過封裝了原生的PreparedStatement。讀寫分離最關鍵的地方在上圖標綠色的地方,也就是生成PreparedStatement的地方。
在使用SQLEcecutionUnit轉換爲PreparedStatement的時候,有一個重要的步驟就是必須先獲取Connection,源碼以下:
public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException { if (getCachedConnections().containsKey(dataSourceName)) { return getCachedConnections().get(dataSourceName); } DataSource dataSource = shardingContext.getShardingRule().getDataSourceRule().getDataSource(dataSourceName); Preconditions.checkState(null != dataSource, "Missing the rule of %s in DataSourceRule", dataSourceName); String realDataSourceName; if (dataSource instanceof MasterSlaveDataSource) { NamedDataSource namedDataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType); realDataSourceName = namedDataSource.getName(); if (getCachedConnections().containsKey(realDataSourceName)) { return getCachedConnections().get(realDataSourceName); } dataSource = namedDataSource.getDataSource(); } else { realDataSourceName = dataSourceName; } Connection result = dataSource.getConnection(); getCachedConnections().put(realDataSourceName, result); replayMethodsInvocation(result); return result; }
若是發現數據源對象爲MasterSlaveDataSource類型,則會使用以下方式獲取真正的數據源:
public NamedDataSource getDataSource(final SQLType sqlType) { if (isMasterRoute(sqlType)) { DML_FLAG.set(true); return new NamedDataSource(masterDataSourceName, masterDataSource); } String selectedSourceName = masterSlaveLoadBalanceStrategy.getDataSource(name, masterDataSourceName, new ArrayList<>(slaveDataSources.keySet())); DataSource selectedSource = selectedSourceName.equals(masterDataSourceName) ? masterDataSource : slaveDataSources.get(selectedSourceName); Preconditions.checkNotNull(selectedSource, ""); return new NamedDataSource(selectedSourceName, selectedSource); } private static boolean isMasterRoute(final SQLType sqlType) { return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly(); }
有三種狀況會認爲必定要走主庫:
1. 不是查詢類型的語句,好比更新字段
2. DML_FLAG變量爲true的時候
3. 強制Hint方式走主庫
當執行了更新語句的時候,isMasterRoute()==true,這時候,Connection爲主庫的鏈接,而且引擎會強制設置DML_FLAG的值爲true,這樣一個請求後續的全部讀操做都會走主庫。
有些時候,咱們想強制走主庫,這時候在請求最開始執行Hint操做便可,以下所示:
HintManager hintManager = HintManager.getInstance(); hintManager.setMasterRouteOnly();
在獲取數據源的時候,若是走的是從庫,會使用從庫負載均衡算法類進行處理,該類的實現比較簡單,以下所示:
public final class RoundRobinMasterSlaveLoadBalanceStrategy implements MasterSlaveLoadBalanceStrategy { private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>(); @Override public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) { AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0); COUNT_MAP.putIfAbsent(name, count); count.compareAndSet(slaveDataSourceNames.size(), 0); return slaveDataSourceNames.get(count.getAndIncrement() % slaveDataSourceNames.size()); } }
其實就是一個簡單的輪循機制進行從庫的負載均衡。
sharding-jdbc進行主從讀寫分離的特性實現比較簡潔易懂,對spring這種上層框架而言是無感知的,並且因爲它是在路由獲得SQLExecutionUtil後再處理的,因此使用了讀寫分離特性,能夠同時使用分庫分表。
sharding-jdbc官方文檔和demo