SpringBoot+shardingsphere實現(xiàn)按月分表功能教程
ShardingSphere 是一套開源的分布式數(shù)據(jù)庫中間件解決方案,旨在簡化數(shù)據(jù)庫分片、讀寫分離、分布式事務(wù)等復(fù)雜場景的管理。
它由 Apache 軟件基金會支持,廣泛應(yīng)用于需要處理大規(guī)模數(shù)據(jù)的系統(tǒng)中
一、ShardingSphere 是什么?
主要是為了防止一張表的數(shù)據(jù)量過大而設(shè)計的,數(shù)據(jù)庫本身就支持,但是由于自行設(shè)計需要滿足跨表查詢,事務(wù)一致性,分頁聚合等很多的復(fù)雜場景,還需要很多的配套監(jiān)控,設(shè)計,擴(kuò)容等方案,所以總體來說是一個任務(wù)量很大的任務(wù),故而這里采用ShardingSphere 來實現(xiàn)。
二、使用步驟
1.引入庫
<!-- 分庫分表 --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.2.0</version> </dependency>
2.環(huán)境配置+Mysql表
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `gender` tinyint(4) NOT NULL COMMENT '0:男 1:女', `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間', `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1890651990057906179 DEFAULT CHARSET=utf8mb4;
# 配置服務(wù)器端口 server: port: 9999 # Spring框架下的ShardingSphere配置 spring: shardingsphere: # 模式配置,設(shè)置為獨立模式 mode: type: Standalone # 數(shù)據(jù)源配置 datasource: # 定義數(shù)據(jù)源名稱 names: ds0 # 數(shù)據(jù)源ds0的具體配置 ds0: # 數(shù)據(jù)源類型為HikariCP type: com.zaxxer.hikari.HikariDataSource # 數(shù)據(jù)庫驅(qū)動類名稱 driver-class-name: com.mysql.cj.jdbc.Driver # 數(shù)據(jù)庫連接URL,包含時區(qū)設(shè)置 jdbc-url: jdbc:mysql://localhost:3306/sharding_db?serverTimezone=Asia/Shanghai # 數(shù)據(jù)庫用戶名 username: root # 數(shù)據(jù)庫密碼 password: root # 規(guī)則配置 rules: # 分片規(guī)則配置 sharding: # 定義分片的表 tables: user: # 只配置基礎(chǔ)表,其他表會動態(tài)創(chuàng)建 actual-data-nodes: ds0.user,ds0.user_202401,ds0.user_202402,ds0.user_202403,ds0.user_202404,ds0.user_202405 table-strategy: standard: sharding-column: createtime sharding-algorithm-name: user_inline # 添加主鍵生成策略 key-generate-strategy: column: id key-generator-name: snowflake sharding-algorithms: user_inline: type: CLASS_BASED props: strategy: standard algorithmClassName: com.hhh.sharding.standa.UserShardingAlgorithm # 配置主鍵生成器 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 123 # 添加默認(rèn)分片策略 default-sharding-column: gender # 屬性配置 props: # 是否顯示SQL語句 sql-show: true # MyBatis-Plus配置 mybatis-plus: configuration: # 不將下劃線轉(zhuǎn)換為駝峰命名 map-underscore-to-camel-case: false # 使用標(biāo)準(zhǔn)輸出日志實現(xiàn) log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: enable-sql-runner: true
這里有一個注意事項,那就是id一定要使用bigint使用雪花策略算法來實現(xiàn),至于為什么這樣呢,是為了防止分表的主鍵id一致的情況,這里首先推薦就是使用mybatisPlus來實現(xiàn),因為他天然支持雪花算法
3.分表代碼實現(xiàn)
主要是兩個文件一個是自己實現(xiàn)分表算法的UserShardingAlgorithm文件
package com.hhh.sharding.standa; import com.baomidou.mybatisplus.extension.toolkit.SqlRunner; import com.hhh.sharding.domain.User; import com.hhh.sharding.service.UserService; import lombok.extern.slf4j.Slf4j; import lombok.var; import org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection; import org.apache.shardingsphere.infra.metadata.database.rule.ShardingSphereRuleMetaData; import org.apache.shardingsphere.mode.manager.ContextManager; import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration; import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration; import org.apache.shardingsphere.sharding.rule.ShardingRule; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.sql.DataSource; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; @Component @Slf4j public class DynamicShardingManager { @Resource private DataSource dataSource; @Resource private UserService userService; private static final String LOGIC_TABLE_NAME = "user"; private static final String DATABASE_NAME = "sharding_db"; // 配置文件中的數(shù)據(jù)庫名稱 @PostConstruct public void initialize() { log.info("初始化動態(tài)分表配置..."); updateShardingTableNodes(); } /** * 獲取所有用戶相關(guān)的表名 * 此方法旨在動態(tài)地收集所有用戶表的表名,以支持可能存在的不同性別用戶表 * 如果無法獲取動態(tài)表名或列表為空,則默認(rèn)返回包含單一的默認(rèn)用戶表名"user" * * @return 包含所有用戶表名的集合 */ private Set<String> fetchAllUserTableNames() { //獲取所有動態(tài)化表名 Set<String> tableNames = new HashSet<>(); try { // 獲取用戶列表 List<User> users = userService.list(); // 如果用戶列表不為空,則映射每個用戶到對應(yīng)的表名,并收集到集合中 if (users != null) { tableNames = users.stream() .map(user -> "user_" + user.getGender()) .collect(Collectors.toSet()); } // 確保至少包含默認(rèn)表 tableNames.add("user"); } catch (Exception e) { // 記錄獲取表名時發(fā)生的錯誤 log.error("獲取所有動態(tài)化表名失敗", e); // 發(fā)生異常時至少返回默認(rèn)表 tableNames.add("user"); } // 返回收集到的表名集合 return tableNames; } /** * 動態(tài)更新分片表節(jié)點配置 * * 本方法旨在根據(jù)當(dāng)前的用戶表名稱,動態(tài)地更新分片表的節(jié)點配置 * 它首先獲取所有用戶表的名稱,然后構(gòu)建新的分片表節(jié)點配置,并嘗試更新到數(shù)據(jù)庫的元數(shù)據(jù)中 */ private void updateShardingTableNodes() { try { // 獲取所有用戶表的名稱 Set<String> tableNames = fetchAllUserTableNames(); if (tableNames.isEmpty()) { // 如果未獲取到任何表名,則使用默認(rèn)的表配置 log.warn("未獲取到任何表名,將使用默認(rèn)表配置"); tableNames.add("user"); } // 確保包含所有可能的表 tableNames.add("user"); tableNames.add("user_0"); tableNames.add("user_1"); // 構(gòu)建新的分片表節(jié)點配置 String newActualDataNodes = tableNames.stream() .distinct() .map(tableName -> "ds0." + tableName) .collect(Collectors.joining(",")); log.info("動態(tài)分表 actual-data-nodes 配置: {}", newActualDataNodes); // 獲取 ContextManager 實例 ContextManager contextManager = getContextManager(); if (contextManager == null) { log.error("獲取 ContextManager 失敗"); return; } // 獲取 MetaDataContexts 實例 var metaDataContexts = contextManager.getMetaDataContexts(); if (metaDataContexts == null) { log.error("獲取 MetaDataContexts 失敗"); return; } // 獲取 MetaData 實例 var metaData = metaDataContexts.getMetaData(); if (metaData == null) { log.error("獲取 MetaData 失敗"); return; } // 檢查數(shù)據(jù)庫是否存在 var databases = metaData.getDatabases(); if (databases == null || !databases.containsKey(DATABASE_NAME)) { log.error("數(shù)據(jù)庫 {} 不存在", DATABASE_NAME); return; } // 獲取 ShardingSphere 的規(guī)則元數(shù)據(jù) ShardingSphereRuleMetaData ruleMetaData = databases.get(DATABASE_NAME).getRuleMetaData(); if (ruleMetaData == null) { log.error("獲取規(guī)則元數(shù)據(jù)失敗"); return; } // 查找 ShardingRule Optional<ShardingRule> shardingRule = ruleMetaData.findSingleRule(ShardingRule.class); if (shardingRule.isPresent()) { // 獲取分片規(guī)則配置 ShardingRuleConfiguration ruleConfig = (ShardingRuleConfiguration) shardingRule.get().getConfiguration(); if (ruleConfig.getTables() == null || ruleConfig.getTables().isEmpty()) { log.error("分片規(guī)則配置為空"); return; } // 更新分片表規(guī)則配置 List<ShardingTableRuleConfiguration> updatedRules = ruleConfig.getTables() .stream() .map(oldTableRule -> { if (LOGIC_TABLE_NAME.equals(oldTableRule.getLogicTable())) { ShardingTableRuleConfiguration newTableRuleConfig = new ShardingTableRuleConfiguration(LOGIC_TABLE_NAME, newActualDataNodes); newTableRuleConfig.setDatabaseShardingStrategy(oldTableRule.getDatabaseShardingStrategy()); newTableRuleConfig.setTableShardingStrategy(oldTableRule.getTableShardingStrategy()); newTableRuleConfig.setKeyGenerateStrategy(oldTableRule.getKeyGenerateStrategy()); newTableRuleConfig.setAuditStrategy(oldTableRule.getAuditStrategy()); return newTableRuleConfig; } return oldTableRule; }) .collect(Collectors.toList()); ruleConfig.setTables(updatedRules); // 嘗試更新分片規(guī)則配置 try { contextManager.alterRuleConfiguration(DATABASE_NAME, Collections.singleton(ruleConfig)); contextManager.reloadDatabase(DATABASE_NAME); log.info("動態(tài)分表規(guī)則更新成功!"); } catch (Exception e) { log.error("更新分片規(guī)則失敗", e); } } else { log.error("未找到 ShardingSphere 的分片規(guī)則配置,動態(tài)分表更新失敗。"); } } catch (Exception e) { log.error("更新分片規(guī)則時發(fā)生異常", e); } } /** * 獲取 ShardingSphere ContextManager */ private ContextManager getContextManager() { try { if (dataSource == null) { log.error("數(shù)據(jù)源未注入"); return null; } var connection = dataSource.getConnection(); if (connection == null) { log.error("獲取數(shù)據(jù)庫連接失敗"); return null; } ShardingSphereConnection shardingConnection = connection.unwrap(ShardingSphereConnection.class); if (shardingConnection == null) { log.error("無法獲取 ShardingSphereConnection"); connection.close(); return null; } ContextManager contextManager = shardingConnection.getContextManager(); connection.close(); return contextManager; } catch (SQLException e) { log.error("獲取 ShardingSphere ContextManager 失敗", e); return null; } } /** * 根據(jù)用戶信息創(chuàng)建用戶表 * 表名基于用戶創(chuàng)建時間生成,格式為:LOGIC_TABLE_NAME_YYYYMM * 如果表已存在,則不進(jìn)行創(chuàng)建操作 * * @param user 用戶對象,包含用戶創(chuàng)建時間等信息 */ public void createUserTable(User user) { // 獲取用戶創(chuàng)建時間 Date createTime = user.getCreatetime(); // 創(chuàng)建日期格式化對象,用于生成表名 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM"); // 生成完整的表名 String tableName = LOGIC_TABLE_NAME + "_" + dateFormat.format(createTime); try { // 首先檢查表是否已存在 String checkTableSql = "SHOW TABLES LIKE '" + tableName + "'"; List<Map<String, Object>> tables = SqlRunner.db().selectList(checkTableSql); // 如果表存在,記錄日志并結(jié)束方法 if (tables != null && !tables.isEmpty()) { log.info("表 {} 已經(jīng)存在,無需創(chuàng)建", tableName); return; } // 創(chuàng)建表 String createTableSql = "CREATE TABLE IF NOT EXISTS " + tableName + " LIKE user"; log.info("開始創(chuàng)建表,SQL: {}", createTableSql); SqlRunner.db().update(createTableSql); log.info("表 {} 創(chuàng)建成功", tableName); // 更新分片配置 updateShardingTableNodes(); } catch (Exception e) { log.error("創(chuàng)建分表 {} 失敗: {}", tableName, e.getMessage(), e); // 檢查異常消息,如果表已存在,則記錄日志并結(jié)束方法 if (e.getMessage() != null && e.getMessage().contains("already exists")) { log.info("表 {} 已經(jīng)存在,繼續(xù)處理", tableName); return; } // 如果異常與表已存在無關(guān),則拋出運行時異常 throw new RuntimeException("創(chuàng)建分表失敗: " + e.getMessage(), e); } } }
package com.hhh.sharding.standa; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Properties; @Slf4j public class UserShardingAlgorithm implements StandardShardingAlgorithm<Date> { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMM"); @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> preciseShardingValue) { Date createTime = preciseShardingValue.getValue(); String logicTableName = preciseShardingValue.getLogicTableName(); log.info("分片算法執(zhí)行 - 可用目標(biāo)表: {}, 分片值: {}, 邏輯表名: {}", availableTargetNames, createTime, logicTableName); if (createTime == null) { log.info("createTime為空,返回邏輯表名: {}", logicTableName); return logicTableName; } // 根據(jù) createTime 動態(tài)生成分表名 String suffix = DATE_FORMAT.format(createTime); String realTableName = "user_" + suffix; log.info("計算得到的實際表名: {}", realTableName); if (availableTargetNames.contains(realTableName)) { log.info("找到匹配的目標(biāo)表: {}", realTableName); return realTableName; } else { log.warn("未找到匹配的目標(biāo)表,返回邏輯表名: {}", logicTableName); return logicTableName; } } @Override public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Date> rangeShardingValue) { return new ArrayList<>(); } @Override public Properties getProps() { return new Properties(); } @Override public void init(Properties properties) { // 可以添加初始化邏輯 } }
4.測試用例
package com.hhh.sharding.controller; import cn.hutool.core.util.RandomUtil; import com.hhh.sharding.domain.User; import com.hhh.sharding.service.UserService; import com.hhh.sharding.standa.DynamicShardingManager; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Date; import java.util.List; import java.text.SimpleDateFormat; @RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @Resource private DynamicShardingManager dynamicShardingManager; @GetMapping("/add") public Boolean user() { // 創(chuàng)建一些2024年的隨機(jī)日期 Date[] dates = { getDate("2024-01-15"), getDate("2024-02-20"), getDate("2024-03-10"), getDate("2024-04-05"), getDate("2024-05-25") }; for (int i = 0; i < 10; i++) { User user = new User(); user.setUsername(generateRandomUsername()); user.setPassword("123456"); user.setGender(RandomUtil.randomInt(2)); // 隨機(jī)選擇一個2024年的日期 Date randomDate = dates[RandomUtil.randomInt(dates.length)]; user.setCreatetime(randomDate); user.setUpdatetime(randomDate); //這里每一次新增數(shù)據(jù)的時候去判斷是否要創(chuàng)建出來當(dāng)月的數(shù)據(jù)表,這張表一定要在 //application.yml中的actual-data-nodes中去添加 dynamicShardingManager.createUserTable(user); userService.save(user); } return true; } private Date getDate(String dateStr) { try { return new SimpleDateFormat("yyyy-MM-dd").parse(dateStr); } catch (Exception e) { return new Date(); } } // 生成10位隨機(jī)數(shù)字的用戶名 private String generateRandomUsername() { return RandomUtil.randomNumbers(10); // 生成10位數(shù)字 } @GetMapping("/all") public List<User> all() { return userService.list(); } }
5.測試結(jié)果
- 新增數(shù)據(jù)
- 查詢數(shù)據(jù)
- 數(shù)據(jù)庫情況
- 數(shù)據(jù)庫表數(shù)據(jù)展示
總結(jié)
由于公司有一個需求那就是按月來分表展示數(shù)據(jù),看了好多人的文章都沒有效果,最終三天得以解決這個功能,故而寫下此文章。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringMVC 使用JSR-303進(jìn)行校驗 @Valid示例
本篇文章主要介紹了SpringMVC 使用JSR-303進(jìn)行校驗 @Valid示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02SpringBoot集成Tomcat服務(wù)架構(gòu)配置
這篇文章主要為大家介紹了SpringBoot集成Tomcat服務(wù)架構(gòu)配置,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Java中的動態(tài)數(shù)組和棧Vector Stack使用區(qū)別介紹
這篇文章主要為大家介紹了Java中的動態(tài)數(shù)組和棧Vector Stack使用介紹,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10maven配置阿里云倉庫的實現(xiàn)方法(2022年)
本文主要介紹了maven配置阿里云倉庫的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03