sharding-method全數(shù)據(jù)庫兼容的服務(wù)層 Sharding 框架
核心特性
全數(shù)據(jù)庫全SQL兼容、完美RR級別讀寫分離、與原生一致的ACID特性、輕量簡單易擴展
另外一個輪子的意義
很多人會質(zhì)疑 市面上較為流行的Sharding中間件/應(yīng)用層Sharing框架已經(jīng)有很多,他們都已經(jīng)發(fā)展了很久了,功能也很強大,為什么要一個再重復(fù)制造這么個輪子呢?
之所以這里有一個新的輪子并不是因為我懶得無事,而是我對目前基于傳統(tǒng)RDB上Sharding框架的設(shè)計的理念不太贊同,雖然他們或許都很圓,跑的很快,但是使用不當?shù)脑挘菀追?。我期望的輪子是既能跑的快,但也能跑的穩(wěn)。
我們目前國內(nèi)主流的Sharding框架都是基于SQL來完成,其主要流程:
- 是解析上層傳入的SQL
- 結(jié)合對應(yīng)的分表分庫配置,對傳入的SQL進行改寫并分發(fā)到對應(yīng)的單機數(shù)據(jù)庫上
- 獲得各個單機數(shù)據(jù)庫的返回結(jié)果后,根據(jù)原SQL歸并結(jié)果,返回用戶期待的結(jié)果
這種實現(xiàn)希望提供一個屏蔽底層Sharding邏輯的解決方案,對上層應(yīng)用來說,只有一個RDB,這樣應(yīng)用可以透明訪問多個數(shù)據(jù)庫。
然而,這僅僅只是一個美麗的目標。因種種原因,這些層次的Sharding方案都無法提供跟原生數(shù)據(jù)庫一樣的功能:
- ACID里的A無法保證
- ACID里的C可能被打破
- ACID里的I與原生不一致
- 由于SQL解析復(fù)雜,性能等考慮,很多數(shù)據(jù)庫SQL不支持
正因為存在這些差異,本質(zhì)上,上層應(yīng)用必須明確的知道經(jīng)過此類Sharding方案后得到的查詢結(jié)果、事務(wù)結(jié)果與原生的有啥不一致才能寫出正確可靠的程序。
因此,基于SQL的Sharding方案對應(yīng)用層并不透明。
如果要基于SQL層的框架寫出正確可靠的代碼的話,我們需要遵循一些范式:
- 所有事務(wù)(包括讀、寫)都不能跨庫
- 跨分片的查詢提供的隔離級別與原生不一致
- 某些聚合查詢的性能消耗很大,要慎用
- ......
這些范式對于實際上就是使用Sharding數(shù)據(jù)庫框架時,不透明的表現(xiàn)。而這些表現(xiàn)都是隱式的隱藏于SQL中,難以REVIEW。
而且這些范式對于很多人來說不一定能夠充分理解執(zhí)行的含義,以至于忽略了。
由上面最重要的一點“所有事務(wù)(包括讀、寫)都不能跨庫”決定,一個合理設(shè)計的代碼里絕大多數(shù)的業(yè)務(wù)代碼中數(shù)據(jù)庫訪問都不會跨分區(qū),核心業(yè)務(wù)代碼都在同一分區(qū)內(nèi)進行。 因此,我們大多數(shù)情況下,需要的只是一個協(xié)助我們便捷選擇對應(yīng)分片的一個框架。
因此我的想法很簡單,提供一個方便透明選擇分片、并輔以自動生成ID的框架。對于需要訪問多個分片的少數(shù)業(yè)務(wù),框架提供手段,便捷地獲取所有分片數(shù)據(jù)庫的數(shù)據(jù),并由用戶自行歸并得出所需結(jié)果(簡單的歸并框架可以自動進行)。
基本使用方法
先簡略展示以下框架的基本用法(以下代碼在UT案例中,但為突出重點,有所裁剪)
Service層
@Service
@ShardingContext(dataSourceSet="orderSet",shardingKeyEls="[user].userId",shardingStrategy="@modUserId",generateIdStrategy="@snowflaker",generateIdEls="[user].userId")
public class UserServceImpl {
@Autowired
private UserDaoImpl userDao;
@Transactional
@SelectDataSource
public void updateUser(User user){
userDao.updateUser(user);
}
@Transactional
@SelectDataSource
@GenerateId
public void saveUser(User user){
userDao.saveUser(user);
}
@Transactional(readOnly=true)
@SelectDataSource(keyNameEls="[userId]")
public User findUser(int userId){
return userDao.findUser(userId);
}
public List
findAllUsers(){
return userDao.findAllUsers();
}
public double calcUserAvgAge(){
List
allUsers = userDao.findAllUsers();
return allUsers.stream().mapToInt(u->u.getAge()).average().getAsDouble();
}
}
@ShardingContext表示當前的Service的Sharding上下文,就是說,如果有 選擇數(shù)據(jù)源、Map到各數(shù)據(jù)庫Reduce出結(jié)果、生成ID等操作時,如果某些參數(shù)沒有指定,都從這個ShardingContext里面的配置取
@SelectDataSource表示為該方法內(nèi)執(zhí)行的SQL根據(jù)Sharding策略選擇一個Sharding數(shù)據(jù)源,在方法結(jié)束返回前,不能更改Sharding數(shù)據(jù)源
@GenerateId表示生成ID,并將其賦值到參數(shù)的指定位置
@GenerateId對應(yīng)的邏輯會先執(zhí)行,然后到@SelectDataSource然后到@Transaction
@Transactional(readOnly=true)標簽指定了事務(wù)時只讀的,因此框架會根據(jù)readOnly標志自動選擇讀庫(如果有的話)
從方法calcUserAvgAge可以看到在JDK8的LAMBADA表達式及Stream功能下,JAVA分析處理集合數(shù)據(jù)變得極為簡單,這會大大減少我們自行加工Sharding分片數(shù)據(jù)的復(fù)雜度。
接下來看DAO層
@Component
public class UserDaoImpl {
@Autowired
private JdbcTemplate jdbcTemplate;
public void updateUser(User user){
int update = jdbcTemplate.update("UPDATE `user` SET `name`=? WHERE `user_id`=?;",user.getName(),user.getUserId());
Assert.isTrue(update == 1,"it should be updated!");
}
public User findUser(int userId){
return jdbcTemplate.queryForObject("SELECT * FROM user WHERE user_id = ?", new Object[]{userId}, rowMapper);
}
@Transactional
@MapReduce
public List
findAllUsers(){
return jdbcTemplate.query("SELECT * FROM user", rowMapper);
}
@Transactional(readOnly=true)
@MapReduce
public void findAllUsers(ReduceResultHolder resultHolder){
List
shardingUsers = jdbcTemplate.query("SELECT * FROM user", rowMapper);
resultHolder.setShardingResult(shardingUsers);
}
}
@MapReduce表示該方法將會在每個數(shù)據(jù)分片都執(zhí)行一遍,然后進行數(shù)據(jù)聚合后返回。 對于聚合前后返回的數(shù)據(jù)類型一致的方法,調(diào)用時可以直接從返回值取得聚合結(jié)果。 對于聚合前后返回的數(shù)據(jù)類型不一致的方法,需要傳入一個對象ReduceResultHolder,調(diào)用完成后,通過該對象獲得聚合結(jié)果
默認情況下,框架會提供一個通用Reduce策略,如果是數(shù)字則累加返回,如果是Collection及其子類則合并后返回,如果是MAP則也是合并后返回。 如果該策略不適合,那么用戶可自行設(shè)計指定Reduce策略。
@Transaction表示每一個Sharding執(zhí)行的SQL都處于一個事務(wù)中,并不是表示整個聚合操作是一個整體的事務(wù)。所以,MapReduce最好不要進行更新操作(考慮框架層次限制MapReduce只允許ReadOnly事務(wù))。
@MapReduce執(zhí)行的操作會在@Transaction之前。
優(yōu)點缺點對比
以上是框架的主要使用形式,我們可以從這種實現(xiàn)中發(fā)現(xiàn)服務(wù)層的Sharding有以下好處
- 全數(shù)據(jù)庫、全SQL兼容
- SQL層Sharding無法做到
- 能完美實現(xiàn)讀寫分離
- 基于SQL層實現(xiàn)的Sharding引入讀寫分離后,在上層Service感知的事務(wù)里,存在混亂的隔離級別的問題,其最多實現(xiàn)RC級別讀寫分離(若不在Service層介入相關(guān)輔助代碼的話),而Service層Sharding在Service開始前就能確定該事務(wù)是讀事務(wù),整個讀事務(wù)都在一個讀庫中完成,隔離級別與數(shù)據(jù)庫一致
- 無額外維護DBProxy可用性的負擔(dān)
- 相對于復(fù)雜的SQL解析,實現(xiàn)簡單,相信花個一天就能看完所有代碼,整個框架了如指掌
- 無SQL解析成本,性能更高
- 隔離級別及事務(wù)原子性等特征與使用的數(shù)據(jù)庫一致,無額外學(xué)習(xí)負擔(dān),易于寫出正確的程序
- 框架限制了所有事務(wù)都在單庫進行
- 基于Sql的Sharding即使在非讀寫分離情況下,因其需要歸并多個數(shù)據(jù)庫的結(jié)果,其提供的隔離級別也是混亂的,但這個區(qū)別并沒有顯式的提示到程序員。
當然也存在缺點
劣勢:
- 跨庫查詢需要自行進行結(jié)果聚合
- 是劣勢也是優(yōu)勢
- 劣勢:需要完成額外的聚合代碼
- 優(yōu)勢:但其能能更好的調(diào)優(yōu),使用JDK8的Stream及Lambada表達式,能像寫SQL一樣簡單的完成相關(guān)集合處理
- 跨庫事務(wù)需要自行保證
- 是劣勢也是優(yōu)勢
- 劣勢:需要額外自行實現(xiàn)跨庫事務(wù)
- 優(yōu)勢:目前所有的Sharding框架實現(xiàn)的跨庫事務(wù)都有缺陷或者說限制,如Sharding-JDBC,Mycat等提供的跨庫事務(wù)都并非嚴格意義的ACID,A可能被打破,I也與原生定義的不一樣,程序員不熟悉時就很容易寫出不可靠的代碼。因此自行控制分布式事務(wù),采用顯式的事務(wù)控制或許是更好的選擇??蓞⒖际褂帽救藢懙牧硗庖粋€框架EasyTransaction
- 無法實現(xiàn)單庫分表
- 其實,單庫分表并不是必須的,這可以用數(shù)據(jù)原生的表分區(qū)來實現(xiàn),性能一樣,使用更便捷
具體使用方法
更具體使用案例請參考 測試Package:org.easydevelop.business里的案例
