当前位置: 首页 > news >正文

Sharding-JDBC分库分表之SpringBoot分片策略

Sharding-JDBC系列

1、Sharding-JDBC分库分表的基本使用

2、Sharding-JDBC分库分表之SpringBoot分片策略

3、Sharding-JDBC分库分表之SpringBoot主从配置

前言

前一篇以一个示例分享了Sharding-JDBC的基本使用。在进行分库分表时,可以设置分库分表的分片策略,在示例中,使用的是最简单的inline分片策略。本篇详细的给大家分享一下Sharding-JDBC的分片策略。

核心概念

在开始讲解分片策略之前,先熟悉几个核心概念。

2.1 逻辑表

水平拆分表后,每个小的表的结构都是一样的,这些相同结构的小表可以使用一个逻辑表来表示,是SQL中表的逻辑标识。

如订单表通过主键按照一定规则(如模5)分为tb_order_1到tb_order_5,它们可以使用同一个逻辑表tb_order来标识。

2.2 真实表

在水平拆分的数据库中真实存在的物理表。

如上面的订单表示例,tb_order1到tb_order_5表为真实表。

2.3 分片键

用于将数据库(表)拆分(水平、垂直)的字段即为分片键。

如上面的订单表示例,订单主键为分片键。

如果没有分片键,所有的数据操作将执行全路由,性能较差。除了对单分片字段的支持,Apache ShardingSphere 也支持根据多个字段进行分片。

2.4 分片算法

用于将数据分片的算法,支持 =>=<=><BETWEENIN 进行分片。Sharding-JDBC不断完善实现自动分片算法,在最新的5.5.0中,提供了基于取模、基于哈希取模、基于分片边界、基于分片容量、基于可变时间的分片算法。开发者也可以通过实现基础的分片算法接口,自定义分片算法。

2.5 分片策略

分片策略由分片键和分片算法组成。分片键是从表格的字段中定义,分片算法是相对独立,可自定义的。不同的分片算法,对应了不同的分片策略。

在Sharding-JDBC中,分片策略分为四类,分别为行表达式分片策略、标准分片策略、复合分片策略、Hint分片策略。不同的分片算法构成了不同的分片策略,但所有的算法都归类到上面的四种类型中,只是具体的实现不同。

准备工作

以下以订单为例,分享一下四类分片策略的使用。先创建表tb_order_1、tb_order_2、tb_order_0_0、tb_order_0_1、tb_order_1_0、tb_order_1_1六张表。

行表达式分片策略

行表达式分片策略(InlineShardingStrategy),无需自定义分片算法,框架已默认实现,适用于做简单的分片算法,是四类分片策略中最简单的。

使用时,在配置中使用Groovy表达式,提供对SQL语句中分片键的=和IN的分片操作支持,仅支持单分片键。

4.1 Groovy表达式概述

行表达式的使用非常直观,只需要在配置中使用 ${ expression } 或 $->{ expression } 标识行表达式即可。目前支持数据节点和分片算法这两个部分的配置。行表达式的内容使用的是 Groovy 的语法,Groovy 能够支持的所有操作,行表达式均能够支持。

如:${begin..end} 表示范围区间、${[unit1, unit2, unit_x]} 表示枚举值。

${['tb_order', 'tb_order_goods']}_${1..2},最终解析为:tb_order_1、tb_order_2、tb_order_goods_1、tb_order_goods_2

行表达式中如果出现连续多个 ${ expression } 或 $->{ expression } 表达式,整个表达式最终的结果将会根据每个子表达式的结果进行笛卡尔组合。

4.2 示例

行表达式分片策略使用时非常简洁,分片配置如下:

spring:shardingsphere:datasource:names: order1,order2sharding:tables:tb_order: #逻辑表database-strategy: #分库策略inline:  # 行表达式分片策略sharding-column: member_idalgorithm-expression: order$->{member_id % 2 + 1} #以member_id模2 + 1,如member_id为3,则存放在order2数据库中actual-data-nodes: order$->{1..2}.tb_order_$->{1..2}table-strategy: #分表策略inline:  # 行表达式分片策略sharding-column: order_idalgorithm-expression: tb_order_$->{order_id % 2 + 1}

1)分库策略

spring.shardingsphere.sharding.tables.tb_order.database-strategy.inline

2)分表策略

spring.shardingsphere.sharding.tables.tb_order.table-strategy.inline

3)其他配置

sharding-column分片键,表中的字段

algorithm-expression:算法表达式,Groovy表达式。tb_order_$->{order_id % 2 + 1},表示以order_id模2后加1为分片算法。如order_id为2,则操作tb_order_1的表

详细的示例见:

Sharding-JDBC分库分表的基本使用-CSDN博客

注:行表达式分片策略仅支持SQL中分片键包含 =、IN 的分片处理,对于 > 、< 等的SQL语句,系统会报错。

标准分片策略

标准分片策略(StandardShardingStrategy),用于处理使用单一键作为分片键的 =、IN、BETWEEN AND、>、<、>=、<= 进行分片的场景。

标准分片策略提供了PreciseShardingAlgorithm(精准分片)和 RangeShardingAlgorithm(范围分片)两种分片算法接口。

使用时,精准分片算法是必现实现的算法,用于处理SQL中分片键含=、IN 的分片处理。范围分片算法用于处理SQL中分片键含 >、<、>=、<= 、BETWEEN AND的分片处理,是非必选的。

注:如果没有实现范围分片算,而SQL中使用了BETWEEN AND的分片处理,那么系统会报错。

说明:在最新的5.5.0的版本,标准分片策略整合了精准分片和范围分片,对应的算法接口为StandardShardingAlgorithm。

5.1 精准分片算法

通过实现PreciseShardingAlgorithm接口实现精准分片算法,该接口只有一个方法,源码如下:

public interface PreciseShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {/*** @param availableTargetNames 可用的datasource或table的名称集合* @param shardingValue sql中,传入的分片键的值* @return*/String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<T> shardingValue);
}

对于数据库分片,第一个参数为配置的可用的数据库的名称集合;对于表分片,第一个参数为配置的可用的表的名称集合。所以对于数据库分片和表分片,实现逻辑一样,此处以表分片为例。

5.1.1 自定义表分片算法

package com.jingai.sharing.jdbc.algorithm;public class StandardShardingAlgorithm implements PreciseShardingAlgorithm<Long> {@Overridepublic String doSharding(Collection<String> tableNames, PreciseShardingValue<Long> preciseShardingValue) {for (String tableName : tableNames) {String mod = preciseShardingValue.getValue() % tableNames.size() + 1 + Strings.EMPTY;if(tableName.endsWith(mod)) {return tableName;}}throw new IllegalArgumentException("找不到匹配的表分片");}
}

以上通过分片键对真实表个数取模加1作为分片的算法。

如:有两个表tb_order_1和tb_order_2,传入的分片键的值为3,则返回tb_order_2的表名字符串

5.1.2 分片配置

spring:shardingsphere:sharding:tables:tb_order: #逻辑表table-strategy:standard:   sharding-column: order_id   #分片键。对id进行分表precise-algorithm-class-name: com.jingai.sharing.jdbc.algorithm.StandardShardingAlgorithm  #精准分片算法

配置方式是在对应的datasource-strategy或table-strategy后面用standard标识标准分片策略。

standard:标识标准分片策略

precise-algorithm-class-name:指定精准分片算法的全路径类名

5.1.3 结果小结

1)精准分片算法根据每个分片键的值,返回分片键所在的表。只处理SQL中分片键含=、IN 的分片处理;

1.1)如果是=的分片,执行一次精准分片算法;

1.2)如果是IN的分片,IN中有几个值,执行几次精准分片算法;

根据返回的最终真实表的个数,分表执行对应SQL操作。同一个真实表的多次操作会进行合并处理,如:IN中传入1、2、3三个数,执行3次的精准分片算法,分别返回tb_order_2、tb_order_1、tb_order_2,则最终执行两条查询。

2)对于非=、IN的分片处理,系统报错;

5.2 范围分片算法

通过实现RangeShardingAlgorithm接口实现范围分片算法,该接口只有一个方法,源码如下:

public interface RangeShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {/*** @param availableTargetNames 可用的datasource或table的名称集合* @param shardingValue sql中,传入的分片键的值区间* @return*/Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<T> shardingValue);
}

通精准分片算法一样,数据库分片和表分片用的同一个接口。此处以表分片为例。

5.2.1 自定义表分片算法

package com.jingai.sharing.jdbc.algorithm;import com.google.common.collect.Range;
import org.apache.logging.log4j.util.Strings;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;import java.util.Collection;
import java.util.HashSet;
import java.util.Set;public class StandardShardingAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {@Overridepublic String doSharding(Collection<String> tableNames, PreciseShardingValue<Long> preciseShardingValue) {for (String tableName : tableNames) {String mod = preciseShardingValue.getValue() % tableNames.size() + 1 + Strings.EMPTY;if(tableName.endsWith(mod)) {return tableName;}}throw new IllegalArgumentException("找不到匹配的表分片");}@Overridepublic Collection<String> doSharding(Collection<String> tableNames, RangeShardingValue<Long> rangeShardingValue) {Range<Long> valueRange = rangeShardingValue.getValueRange();Long lower = valueRange.hasLowerBound() ? valueRange.lowerEndpoint() : null;Long upper = valueRange.hasUpperBound() ? valueRange.upperEndpoint() : null;// 如果区间不确定,则需要全表操作if(lower == null || upper == null) {return tableNames;}Set<String> rs = new HashSet<>();// 循环计算需要用到的表for (long i = lower; i < upper ; i ++) {for(String name : tableNames) {String mod = i % tableNames.size() + 1 + Strings.EMPTY;if(name.endsWith(mod)) {rs.add(name);}}}return rs;}
}

以上代码为完整的标准分片策略算法,同时实现了精准分片算法和范围分片算法。算法的逻辑都是按照分片键取模加1。

5.2.2 分片配置

spring:shardingsphere:datasource:names: order1 #数据源名称,有几个数据源就写几个名字,和url中的数据库名字保持一致order1:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/shardingjdbctest?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=falseusername: rootpassword: 123456#分表策略#按照id分表,id使用雪花算保证全局唯一,具体算法:tb_order是表前缀,拼接上:$->{id % 2}  的值sharding:tables:tb_order: #逻辑表actual-data-nodes: order1.tb_order_$->{1..2}  #order1:数据源名称;两个tb_order表,分别为tb_order_1和tb_order_2key-generator: # 指定主键生成策略column: order_idtype: SNOWFLAKEtable-strategy:standard:   # 标准分片策略sharding-column: order_id   #分片键。对id进行分表precise-algorithm-class-name: com.jingai.sharing.jdbc.algorithm.StandardShardingAlgorithm  #精准分片算法range-algorithm-class-name: com.jingai.sharing.jdbc.algorithm.StandardShardingAlgorithm  #范围分片算法

range-algorithm-class-name:指定范围分片算法的全路径类名

5.2.3 结果小结

对于分片键含 >、<、>=、<= 的分片,在范围分片算法中的ValueRange中,lower和upper只有其中一个有值;

复合分片策略

复合分片策略(ComplexShardingStrategy),用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。处理分片键中含有 =、IN、BETWEEN AND、>、<、>=、<= 等操作符进行分片的场景。

复合分片策略提供了ComplexKeysShardingAlgorithm接口,通过重写doSharding()方法实现复合分片算法。源码如下:

public interface ComplexKeysShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {/*** @param availableTargetNames 可用的datasource或table的名称集合* @param shardingValue 存放分片键及对应的值集合或值区间* @return*/Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<T> shardingValue);
}

数据库分片和表分片都是实现同一个接口,此处以表按member_id和order_id分片为例。

6.1 自定义表分片算法

package com.jingai.sharing.jdbc.algorithm;import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;import java.util.Collection;
import java.util.HashSet;
import java.util.Map;public class OrderComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {@Overridepublic Collection<String> doSharding(Collection<String> tableNames, ComplexKeysShardingValue<Long> complexKeysShardingValue) {// IN、= 的处理Map<String, Collection<Long>> valuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();Collection<Long> memberIds = valuesMap.containsKey("member_id") ? valuesMap.get("member_id") : new HashSet<>();Collection<Long> orderIds = valuesMap.containsKey("order_id") ? valuesMap.get("order_id") : new HashSet<>();// 区间的处理Map<String, Range<Long>> rangeMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();if(rangeMap.containsKey("member_id")) {memberIds.addAll(getRangValue(rangeMap.get("member_id"), tableNames.size()));}if(rangeMap.containsKey("order_id")) {orderIds.addAll(getRangValue(rangeMap.get("order_id"), tableNames.size()));}return getTableNames(memberIds, orderIds, tableNames);}private Collection<Long> getRangValue(Range<Long> range, int tableNameSize) {Long lower = range.hasLowerBound() ? range.lowerEndpoint() : null;Long upper = range.hasUpperBound() ? range.upperEndpoint() : null;Collection<Long> rs = new HashSet<>(tableNameSize);if(lower == null || upper == null) {lower = 1l;upper = (long)tableNameSize;} else {upper = upper > (lower + tableNameSize) ? (lower + tableNameSize) : upper;}for (long i = lower ; i <= upper ; i ++) {rs.add(i);}return rs;}private Collection<String> getTableNames(Collection<Long> memberIds, Collection<Long> orderIds, Collection<String> tableNames) {int size = tableNames.size();Collection<String> rs = new HashSet<>(size < 16 ? size : 16);// 通过对memberId和orderId分别取模进行分库size = size / 2; // 两个分片键String underline = "_";// 如果没有memberId值,则根据orderId扫描,不处理系统会报错if(memberIds.isEmpty()) {for(long orderId : orderIds) {String suffix = underline + orderId % size;for(String tableName : tableNames) {if(tableName.endsWith(suffix)) {rs.add(tableName);break;}}}} else if(orderIds.isEmpty()) {for(long memberId : memberIds) {String middle = underline + memberId % size + underline;for(String tableName : tableNames) {if(tableName.contains(middle)) {rs.add(tableName);break;}}}} else {for (long memberId : memberIds) {for(long orderId : orderIds) {String suffix = memberId % size + "_" + orderId % size;for(String tableName : tableNames) {if(tableName.endsWith(suffix)) {rs.add(tableName);break;}}}}}return rs;}
}

在ComplexKeysShardingAlgorithm接口中,包含了精准和范围分片的算法。算法为先按member_id取模,然后再按order_id取模。如果只有member_id,没有order_id,则对member_id下的所有表操作;如果只有order_id没有member_id,则对order_id下的所有member的表操作。

注:在自定义分片算法时,对于存在分片键的操作(即进入了自定义分片算法),都需要有返回值,否则系统会报找不到路由信息的异常错误。

6.2 分片配置

# 单数据库,inline分片策略测试
server:port: 8080#sharding-jdbc分片规则配置
spring:shardingsphere:datasource:names: order1 #数据源名称,有几个数据源就写几个名字,和url中的数据库名字保持一致order1:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/shardingjdbctest?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=falseusername: rootpassword: 123456#分表策略#按照id分表,id使用雪花算保证全局唯一sharding:tables:tb_order: #逻辑表actual-data-nodes: order1.tb_order_$->{0..1}_$->{0..1}  #order1:数据源名称;四个tb_order表,分别为tb_order_0_0到tb_order_1_1key-generator: # 指定主键生成策略column: order_idtype: SNOWFLAKEtable-strategy:complex:   # 复合分片策略sharding-columns: member_id, order_id   #分片键algorithm-class-name: com.jingai.sharing.jdbc.algorithm.OrderComplexShardingAlgorithm  #分片算法props:sql:show: true  # 是否打印sql

配置方式是在对应的datasource-strategy或table-strategy后面用complex标识复合分片策略。

complex:表示复合分片策略

sharding-columns指定复合分片算法的分片键,多个中间用","隔开

algorithm-class-name指定复合分片算法的全路径类名

Hint分片策略

Hint分片策略(HintShardingStrategy),用于处理使用 Hint 行分片的场景。相比另外三种策略,该分片策略无需配置分片键,而是在执行数据库操作之前,进行分片信息的设定,使得数据库操作指定的分库、分表中执行。

Hint分片策略通过Hint API实现指定操作,使得分片规则变成个性化配置。

Hint分片策略提供了HintShardingAlgorithm接口,实现自定义Hint分片算法。源码如下:

public interface HintShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {/*** @param availableTargetNames 可用的datasource或table的名称集合* @param shardingValue 分片键及设置的分片键的值* @return*/Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<T> shardingValue);
}

数据库分片和表分片都是实现同一个接口,此处以表按member_id和order_id分片为例。

7.1 自定义表分片算法

public class OrderHintShardingAlgorithm implements HintShardingAlgorithm<Integer> {@Overridepublic Collection<String> doSharding(Collection<String> tableNames, HintShardingValue<Integer> hintShardingValue) {Collection<String> rs = new HashSet<>();for(Integer val : hintShardingValue.getValues()) {String suffix = "_" + (val % tableNames.size() + 1);for(String tableName : tableNames) {if(tableName.endsWith(suffix)) {rs.add(tableName);break;}}}return rs.isEmpty() ? tableNames : rs;}
}

算法比较简单,以设置的分片键的值取模。

7.2 分片配置

# 单数据库,inline分片策略测试
server:port: 8080#sharding-jdbc分片规则配置
spring:shardingsphere:datasource:names: order1 #数据源名称,有几个数据源就写几个名字,和url中的数据库名字保持一致order1:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/shardingjdbctest?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=falseusername: rootpassword: 123456#分表策略#按照id分表,id使用雪花算保证全局唯一,具体算法:tb_order是表前缀,拼接上:$->{id % 2}  的值sharding:tables:tb_order: #逻辑表actual-data-nodes: order1.tb_order_$->{1..2}  #order1:数据源名称;两个tb_order表,分别为tb_order_1和tb_order_2key-generator: # 指定主键生成策略column: order_idtype: SNOWFLAKEtable-strategy:hint:  # hint分片策略algorithm-class-name: com.jingai.sharing.jdbc.algorithm.OrderHintShardingAlgorithm  #分片算法props:sql:show: true  # 是否打印sql

7.3 方法

@RestController
public class OrderController {@Resourceprivate OrderService orderService;@RequestMapping("order")public String order(OrderEntity order) {// 清除上一次的规则HintManager.clear();// 指定插入到1的表HintManager hintManager = HintManager.getInstance();hintManager.addTableShardingValue("tb_order", 1);order.setOrderTime(new Date());long insert = orderService.insert2(order);return insert > 0 ? "success" : "fail";}
}

在执行插入之前,通过HintManager的addTableShardingValue()指定如果是逻辑表为tb_order,则存放在分片键值为1的小表中,结合上面的算法,存放在tb_order_2表中。无论执行多少次这个方法,数据都是存放在tb_order_2这个表。可以执行多次addTableShardingValue()方法,添加多个分片键值。

HintManager在使用之前都需要先执行HintManager.clear(),因为设置的信息是保存在ThreadLocal,可能会冲突。

addDatabaseShardingValue():添加数据库分片键值;

addTableShardingValue():添加表分片键值;

setMasterRouteOnly():在读写分离的数据库中,强制从主库中读取。在最新的5.5.0版本,该方法被setWriteRouteOnly()取代;

小结

以上为本篇分享的全部内容。以下做个小结:

1)行表达式分片策略只适合分片键中包含=、IN的分片处理,无需自定义分片算法,只支持单分片键;

2)标准分片策略适合分片键中包含 =、IN、BETWEEN AND、>、<、>=、<=的分片处理,需自定义分片算法,只支持单分片键;

3)复合分片策略适合分片键中包含 =、IN、BETWEEN AND、>、<、>=、<=的分片处理,需自定义分片算法,支持多个分片键;

4)Hint分片策略适合所有操作的分片处理,需自定义分片算法,可在每次执行前通过HintManager个性化设置分片键值;

5)对于存在分片键的数据库操作,如果没有返回对应的分片库或分片表,则系统会提示找不到路由信息的异常;

以上的示例的完整代码可以结合

Sharding-JDBC分库分表的基本使用-CSDN博客

博文中的示例,是在此基础上修改了配置文件。

说明:以上分片策略算法只是为了演示功能,实际项目的分片需要根据业务需要进行设计。特别是复合分片策略的算法,在实际项目中,应该没人会如此设计。

关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 前端实现无缝自动滚动动画
  • 【leetcode刷题笔记】02.复写零
  • 华为HCIP Datacom H12-821 卷28
  • Postman使用教程【项目实战】
  • Java 8革新:现代编程的全新标准与挑战
  • 《梦醒蝶飞:释放Excel函数与公式的力量》10.4 IMREAL函数
  • C#字符串格式化的方式
  • nginx的正向代理和反向代理
  • 微软 Edge 浏览器全解析
  • Kylin系列(三)安装与配置:搭建你的第一个 Kylin 环境
  • 网络防御保护——网络安全概述
  • 第二周:李宏毅机器学习笔记
  • Android知识收集
  • openGauss配置vscode编译调试环境
  • Python: 分块读取文本文件
  • [deviceone开发]-do_Webview的基本示例
  • [译]CSS 居中(Center)方法大合集
  • Angular Elements 及其运作原理
  • Java读取Properties文件的六种方法
  • JS函数式编程 数组部分风格 ES6版
  • Mocha测试初探
  • npx命令介绍
  • oldjun 检测网站的经验
  • Spring Boot MyBatis配置多种数据库
  • Vue全家桶实现一个Web App
  • 不发不行!Netty集成文字图片聊天室外加TCP/IP软硬件通信
  • 讲清楚之javascript作用域
  • 力扣(LeetCode)56
  • 判断客户端类型,Android,iOS,PC
  • 协程
  • 你对linux中grep命令知道多少?
  • 好程序员web前端教程分享CSS不同元素margin的计算 ...
  • #07【面试问题整理】嵌入式软件工程师
  • #LLM入门|Prompt#1.7_文本拓展_Expanding
  • #多叉树深度遍历_结合深度学习的视频编码方法--帧内预测
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (33)STM32——485实验笔记
  • (35)远程识别(又称无人机识别)(二)
  • (C语言)输入一个序列,判断是否为奇偶交叉数
  • (pojstep1.3.1)1017(构造法模拟)
  • (超详细)语音信号处理之特征提取
  • (动态规划)5. 最长回文子串 java解决
  • (附源码)springboot宠物医疗服务网站 毕业设计688413
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (更新)A股上市公司华证ESG评级得分稳健性校验ESG得分年均值中位数(2009-2023年.12)
  • (含笔试题)深度解析数据在内存中的存储
  • (贪心) LeetCode 45. 跳跃游戏 II
  • (转)项目管理杂谈-我所期望的新人
  • .Net Core中的内存缓存实现——Redis及MemoryCache(2个可选)方案的实现
  • .net dataexcel winform控件 更新 日志
  • .NET 分布式技术比较
  • .net 提取注释生成API文档 帮助文档
  • .net6解除文件上传限制。Multipart body length limit 16384 exceeded
  • .Net6支持的操作系统版本(.net8已来,你还在用.netframework4.5吗)
  • .net使用excel的cells对象没有value方法——学习.net的Excel工作表问题