前言 我在上篇博客 “Spring Boot 的實踐與思考” 中比對不同規範的 ORM 框架應用場景的時候提到過主從與讀寫分離,本篇隨筆將針對此和分庫分表進行更深入地探討。 1. 漫談 在進入正題之前,我想先隨意談談對架構的拓展周期的想法(僅個人觀點)。首先,我認為初期規劃不該太複雜或者龐大,無論項 ...
前言
我在上篇博客 “Spring Boot 的實踐與思考” 中比對不同規範的 ORM 框架應用場景的時候提到過主從與讀寫分離,本篇隨筆將針對此和分庫分表進行更深入地探討。
1. 漫談
在進入正題之前,我想先隨意談談對架構的拓展周期的想法(僅個人觀點)。首先,我認為初期規劃不該太複雜或者龐大,無論項目的中長期可能會發展地如何如何,前期都應該以靈活為優先,像分庫分表等操作不應該在開始的時候就考慮進去。其次,我認為需求變更是非常正常的,這點在我等開發的圈子裡吐槽的最多,其中自然有 “領導們” 在業務方面欠缺整體考慮的因素,但我們也不該局限在一個觀點內,市場中變則通,不變則死,前期更是如此,因此在前幾版的架構中我們必須要考慮較高的可擴展性。最後,當項目經過幾輪市場的洗禮和迭代開發,核心業務趨於穩定了,此時我們再結合中長期的規劃給系統來一次重構,細緻地去劃分領域邊界,該解耦的解耦,該拆分的拆分。
2. 分庫分表
2.1 概述
當資料庫達到一定規模後(比如說大幾千萬以上),切分是必須要考慮的。一般來說我們首先要進行垂直切分,即按業務分割,比如說用戶相關、訂單相關、統計相關等等都可以單獨成庫。圖片來源 →
但僅僅如此這是完全不夠的,垂直切分雖然剝離了一定的數據,但每個業務還是那個數量級,因此我們還得採取水平切分進一步分散數據,這也是本節論述的重點。
分庫分表的優點相信上述兩圖都一目瞭然了,一個是專庫專用,業務更集中,另一個是提升資料庫服務的負載能力。But there are always two sides to a coin。 從此以後你要接受你的系統複雜度將提升一個檔次,迭代、遷移、運維等都不再容易。
2.2 切分策略
垂直切分在實現上就是一個多數據源的問題,沒啥好講的。以下 Demo 為水平切分,基於 Sharding-JDBC 中間件,我只做邏輯上的陳述,有關其更詳細的信息和配置請移步 “官方文檔”。
首先,我們得在配置文件中定義分片策略,application.yml:
server:
port: 8001
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mappers/*.xml
sharding:
jdbc:
datasource:
names: youclk_0,youclk_1
youclk_0:
type: org.apache.commons.dbcp.BasicDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://mysql:3306/youclk_0?useSSL=false
username: root
password: youclk
youclk_1:
type: org.apache.commons.dbcp.BasicDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://mysql:3306/youclk_1?useSSL=false
username: root
password: youclk
config:
sharding:
default-database-strategy:
inline:
sharding-column: number
algorithm-expression: youclk_${number % 2}
tables:
user:
actual-data-nodes: youclk_${0..1}.user
具體每個參數的含義在官方文檔有詳細解釋,其實看名稱也能理解個大概了,我定義將 number 為偶數的數據存入 youclk_0,奇數存入 youclk_1。
User:
@Data
public class User {
private String id;
private Integer number;
private Date createTime;
}
UserRepository:
@Mapper
public interface UserRepository {
void insert(User user);
}
UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youclk.data.repository.UserRepository">
<resultMap id="BaseResultMap" type="com.youclk.data.entity.User">
<id column="id" property="id" jdbcType="CHAR"/>
<result column="number" property="number" jdbcType="INTEGER"/>
<result column="createTime" property="create_time" jdbcType="DATE"/>
</resultMap>
<sql id="Base_Column_List">
id, number, createTime
</sql>
<insert id="insert">
INSERT INTO user (
id, number
)
VALUES (
uuid(),
#{number,jdbcType=INTEGER}
)
</insert>
</mapper>
UserService:
@Service
public class UserService {
@Resource
private UserRepository userRepository;
public void insert() {
for (int i = 0; i < 10; i++) {
User user = new User();
user.setNumber(i);
userRepository.insert(user);
}
}
}
Result:
以上做了一個簡單的迴圈插入,可以看到數據已經按策略分庫存儲,結果符合我們的預期。
分庫之後在查詢方面要比之前更加謹慎,既然按策略去切了,那最好就是按策略去查,否則...比如我水平切分了 100個庫,若不按策略去查詢 LIMIT 100000, 10
這麼一組數據,那最後掃描的數量級別是 100 * (100000 + 10)
, 這是比較恐怖的,雖然 Sharding-JDBC 做了一些優化,比如他不是一次性去查詢到記憶體中,而是採用流式處理 + 歸併排序的方式,但仍然比較耗資源,能避免還是儘量去避免吧。
2.3 分散式事務
在任何系統中事務都是頂要緊的事情,面對已分庫的系統更是如此,保證誇庫事務的安全從來不容易。分散式事務的場景有兩種,一個是在分散式服務中,這個後續有機會再探討,本節重點關註誇庫事務。
Sharding-JDBC 自動包含了弱XA事務支持,即能夠保證邏輯上的事務安全,但因網路或硬體導致的異常無法回滾,實現上與一般事務無異:
@Test
@Transactional
public void insertTest() {
userService.insert();
int error = Integer.parseInt("I want error");
userService.insert();
}
可以看到誇庫事務已回滾,除此之外 Sharding-JDBC 還提供了最大努力送達型柔性事務(將執行過程記錄到日誌中,失敗重試,成功後刪除,若最終還是失敗則保留事務日誌,供人工干預),雖然安全性更高,但無法保證時效,限制也很多,這裡留個待續吧,後續有空再深入探討(主要是比較晚了,想早點寫完休息