日常使用數據庫的時候,更多的時間是在關心業務功能的實現,為了盡快完成新版本的發布上線,通常在項目初期不太會去在意數據庫的壓力和性能問題。在服務上線一段時間之后,就會發現當初設計存在著很多的不足,這都是項目研發的正常過程。對于有經驗的程序員,在項目設計初期就會想到將來有一天會遇到這些問題,所以就從一開始就將代碼寫的比較完善,這也是提現大齡程序員優勢的地方。
有一種常見的現象就是,項目上線初期,由于用戶量不多,所以數據庫中的數據也不會太多,服務運行的非常順暢,但是隨著生產數據的積累,很快就導致了數據庫性能瓶頸的到來。這時候我們最先想到的是提升數據庫的資源,加大內存,加大磁盤,從而度過數據庫壓力的難關,但是這畢竟是暫時的,比如一張表里的數據量迅速的增長,而且你又不能刪除這里面的數據,終究有一天會讓這張表爆掉。
以mysql為例,如果單張表的行數超過500萬行的時候,通常就能感受到非常明顯的性能衰減,這點不得不佩服oracle動輒幾億的單表查詢能力,但是沒辦法,兩者價格的差距也是性能的差距。如何應對這種持續增長的單表數據呢?一種常用的方式就是分庫分表,就是把一張巨大的表,按照一定的規則分到不同的表里去,這樣每張分表的數據量就小了,從而保證每個分表的性能,如果分表也不足以支撐大數據量,就通過分庫,把數據量分到多個庫里去,從而支撐住業務功能。
shardingsphere是諸多分庫分表工具中比較優秀的一款,在我經歷過的公司中,也應用在了生產服務中,雖然使用過程中遇到的坑也不少,不過總體來說,還是足夠支撐業務功能。我們首先介紹一下,分庫和分表是兩個截然不同的功能,雖然總混在一起說,分表只要我們在Springboot中引入shardingsphere-jdbc這個依賴庫即可,但是分庫就要單獨部署一個服務shardingsphere-proxy,其他服務連接shardingsphere-proxy,從而實現分庫的功能。
我們先用shardingsphere-jdbc來進行單庫的分表,分表常用的規則有兩種,一種是通過時間進行分表,比如一個月一張表,或者一周一張表,另外一種就根據列的數值進行分表,比如id是1-1000用一張表,1001-2000用一張表,分表的規則要按照業務功能去切分,無論哪種分表策略,最終的目標就是讓數據均勻的分布在各個分表中。
1、創建數據表
我們先創建一張存儲消息的表,過去我們創建消息表就是一張,比如叫sys_message,但是現在我們是用分表,所以就要創建一批表,我們設定消息表使用時間分表策略,每7天一張表,從2025年1月1日開始,所以我們就要創建一張分表sys_message_20250101,然后按照每7天一張表創建出多干個消息分表,這里注意,shardingsphere的分表默認是不自動創建表的,所以我們先手動創建,我制作了一個存儲過程可以快速創建出多張sys_message分表。
sys_message_20250101這一張是分表的基礎表,沒有啟動會報錯,其他的分表即使沒有,啟動的時候也不報錯,但是用到了就會拋出異常。
CREATE TABLE `sys_message_20250101` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',`msg` longblob COMMENT '消息內容',`version` int NOT NULL DEFAULT '1' COMMENT '版本號',`is_logic_delete` int NOT NULL DEFAULT '0' COMMENT '邏輯刪除',`create_by` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '創建人',`create_time` datetime DEFAULT NULL COMMENT '創建時間',`update_by` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',`update_time` datetime DEFAULT NULL COMMENT '修改時間',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1989 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='系統-消息表';
自動創建從2025年1月1日起到一年后的分表存儲過程:
CREATE PROCEDURE `sp_generate_message_tables`(IN start_date DATE)
BEGINDECLARE end_date DATE DEFAULT DATE_ADD(CURRENT_DATE(), INTERVAL 1 YEAR);DECLARE item_date DATE;DECLARE table_name VARCHAR(50);SET item_date = DATE_ADD(start_date, INTERVAL 7 DAY);WHILE item_date <= end_date DOSET table_name = CONCAT('sys_message_', DATE_FORMAT(item_date, '%Y%m%d'));SET @sql = CONCAT('CREATE TABLE IF NOT EXISTS ', table_name, ' LIKE sys_message_', DATE_FORMAT(start_date, '%Y%m%d'));PREPARE stmt FROM @sql;EXECUTE stmt;DEALLOCATE PREPARE stmt;SET item_date = DATE_ADD(item_date, INTERVAL 7 DAY);END WHILE;SELECT CONCAT('分表生成完成,時間范圍:', start_date, ' 至 ', end_date) AS result;
END
2、創建項目shardingsphere-demo
創建一個新項目shardingsphere-demo,并且在pom.xml文件中引入MyBatis-Plus、Shardingsphere和mysql依賴。
<dependencies><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.10.1</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser</artifactId><version>3.5.10.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.4.0</version></dependency><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc</artifactId><version>5.5.2</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.24</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.54</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId><version>4.4.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>RELEASE</version><scope>compile</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
3、創建MyBatis-Plus的各個類
雖然各個分表的表名是不一樣的,但是在代碼里我們并不用去記錄這些表名,而是使用邏輯表名sys_message進行操作,讓Shardingsphere去自動幫我們定位真正的分表。
MessageDO:
package com.mj.shardingsphere.entity;import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;
import java.time.LocalDateTime;/*** 系統-消息表*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_message")
public class MessageDO implements Serializable {/*** id*/@TableId(value = "id", type = IdType.ASSIGN_ID)private Long id;/*** 消息value*/@TableField(value = "msg")private String msg;/*** 版本號*/@Version@TableField(value = "version")private Integer version;/*** 邏輯刪除*/@TableLogic@TableField(value = "is_logic_delete")private Integer logicDelete;/*** 創建人*/@TableField(value = "create_by", fill = FieldFill.INSERT)private String createBy;/*** 創建時間*/@TableField(value = "create_time", fill = FieldFill.INSERT)private LocalDateTime createTime;/*** 修改人*/@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)private String updateBy;/*** 修改時間*/@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;private static final long serialVersionUID = 1L;
}
MessageMapper:
package com.mj.shardingsphere.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mj.shardingsphere.entity.MessageDO;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface MessageMapper extends BaseMapper<MessageDO> {
}
MessageMapper.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.mj.shardingsphere.dao.MessageMapper"><resultMap id="BaseResultMap" type="com.mj.shardingsphere.entity.MessageDO"><!--@mbg.generated--><!--@Table sys_message--><id column="id" jdbcType="BIGINT" property="id" /><result column="msg" jdbcType="VARCHAR" property="msg" /><result column="version" jdbcType="INTEGER" property="version" /><result column="is_logic_delete" jdbcType="INTEGER" property="logicDelete" /><result column="create_by" jdbcType="VARCHAR" property="createBy" /><result column="create_time" jdbcType="TIMESTAMP" property="createTime" /><result column="update_by" jdbcType="VARCHAR" property="updateBy" /><result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /></resultMap><sql id="Base_Column_List"><!--@mbg.generated-->id, msg, version, is_logic_delete, create_by, create_time, update_by, update_time</sql>
</mapper>
4、Shardingsphere配置:
創建一個文件sharding.yml,上半部分就是數據庫的配置,將數據源和連接池交給了Sharding進行管理,Springboot里面就不用再配置了。sys_message_algorithm決定了分表的策略,按照時間分表的時候,要定好分表的時間段,可以寫一個很長的時間。sharding-suffix-pattern是分表的后綴格式,正是因為有這個配置,Sharding才能很好的把所有的分表整合成了一個邏輯表讓我們用分表的時候就像只有一張表一樣。最后就是配置分表時間是7天,這時候數據庫里的分表也要嚴格按照7的跨度去生成。
# 模式配置
mode:type: Standalonerepository:type: JDBC
# 數據源配置
dataSources:sharding:dataSourceClassName: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.18.42:3306/sharding?useSSL=false&useUnicode=true&characterEncoding=UTF-8username: rootpassword: rootdruid:test-on-borrow: truevalidation-query: SELECT 1 FROM DUALweb-stat-filter:enabled: truestat-view-servlet:enabled: truelogin-username: druidlogin-password: 12345pool-prepared-statements: falsemax-pool-prepared-statement-per-connection-size: 20
# 規則配置
rules:# 單表配置- !SINGLEtables:- sharding.*# 數據分片- !SHARDINGtables:sys_message:actualDataNodes: sharding.sys_message_${20250101..20991231}tableStrategy:standard: # 用于單分片鍵的標準分片場景shardingColumn: create_timeshardingAlgorithmName: sys_message_algorithmkeyGenerateStrategy: # 分布式序列策略column: idkeyGeneratorName: snowflakeauditStrategy: # 分片審計策略auditorNames: # 分片審計算法名稱- sharding_key_required_auditorallowHintDisable: true# 分片算法配置shardingAlgorithms:sys_message_algorithm:type: INTERVALprops:datetime-pattern: yyyy-MM-dd HH:mm:ssdatetime-lower: "2025-01-01 00:00:00" # 添加引號確保格式正確datetime-upper: "2099-12-31 23:59:59" # 添加引號確保格式正確sharding-suffix-pattern: yyyyMMdddatetime-interval-amount: 7datetime-interval-unit: DAYS# 分布式序列算法配置keyGenerators:snowflake:type: SNOWFLAKE# 分片審計算法配置auditors:sharding_key_required_auditor:type: DML_SHARDING_CONDITIONSprops:sql-show: true
# sql-simple: false
# max-connections-size-per-query: 1
# check-table-metadata-enabled: false
修改一下application.yml文件,把sharding.yml文件引入進去。
spring:application:name: shardingsphere-demodatasource:type: com.alibaba.druid.pool.DruidDataSource# 引入shardingspheredriver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriverurl: jdbc:shardingsphere:classpath:sharding.yml?placeholder-type=environmentinitialSize: 5minIdle: 5maxActive: 20maxWait: 60000timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQuery: 'SELECT 1 FROM DUAL'testWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20filters: 'stat,wall'connectionProperties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000'useGlobalDataSourceStat: truemybatis-plus:configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: auto# ????logic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0mapper-locations: classpath:/mapper/**.xmlspringdoc:swagger-ui:path: /swagger-ui.htmltags-sorter: alphaoperations-sorter: alphaapi-docs:path: /v3/api-docsgroup-configs:- group: 'default'paths-to-match: '/**'packages-to-scan: com.mj.shardingsphere
knife4j:enable: trueproduction: falsesetting:language: zh_cn
5、編寫服務類
MessageService:
package com.mj.shardingsphere.service;import com.mj.shardingsphere.entity.MessageDO;import java.util.List;public interface MessageService {String sendMessage(String message);List<MessageDO> getMessages();
}
MessageServiceImpl:
這里要注意查詢的使用,由于shardingsphere是從分表里進行操作,所以查詢的時候也是從所有的分表里進行查詢,這是一件很恐怖的事情,所以一定要默認帶著分片鍵并且固定一個區間,讓它從有限的分表里進行查詢,防止因為查詢太多數據而導致服務宕機。這里可能會給業務功能帶來一定的困擾,比如就是不知道查詢多久數據,這時候必須要在業務功能上做讓步。
package com.mj.shardingsphere.service.impl;import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.mj.shardingsphere.dao.MessageMapper;
import com.mj.shardingsphere.entity.MessageDO;
import com.mj.shardingsphere.service.MessageService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.List;@AllArgsConstructor
@Service
public class MessageServiceImpl implements MessageService {private final MessageMapper messageMapper;@Overridepublic String sendMessage(String message) {MessageDO messageDO = new MessageDO();messageDO.setMsg(message);messageDO.setCreateBy("SYSTEM");messageDO.setCreateTime(LocalDateTime.now());messageDO.setUpdateBy("SYSTEM");messageDO.setUpdateTime(LocalDateTime.now());messageMapper.insert(messageDO);return "ok";}@Overridepublic List<MessageDO> getMessages() {//查詢分表的時候,一定要使用分片鍵去固定分表的區間,防止查詢太多的表return messageMapper.selectList(Wrappers.lambdaQuery(MessageDO.class).ge(MessageDO::getCreateTime, LocalDateTime.now().minusMonths(1)).le(MessageDO::getCreateTime, LocalDateTime.now()));}
}
6、測試接口
MessageController:
package com.mj.shardingsphere.controller;import com.mj.shardingsphere.entity.MessageDO;
import com.mj.shardingsphere.service.MessageService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@AllArgsConstructor
@RequestMapping("/message")
@RestController
public class MessageController {private final MessageService messageService;@GetMapping("/add")public String add() {return messageService.sendMessage("message-" + System.currentTimeMillis());}@GetMapping("/list")public List<MessageDO> lst() {return messageService.getMessages();}
}
http://127.0.0.1:8080/message/add?通過調用add接口,通過日志和數據庫,能發現進入到對應時間段內的那個分表了。
http://127.0.0.1:8080/message/list?查詢接口查詢了最近一個月的數據