shardingjdbc全局表_Sharding-JDBC动态分表实现
Sharding-JDBC动态水平分表实现
背景:
在项目中遇到了按照日期动态水平分表的需求,系统属于监控系统,每10分钟保存一次监控数据,并且每次要采集200个节点上的数据,即每次采集数据(间隔10分钟),向数据表添加200条记录,这样一个月数据表就有将近100万条记录。
为了控制单表数据量,并且为了方便后期数据统计,所以,每个月创建一张新表,之后的采集数据都写到新表中,例如报警信息表:alarm_histrtory表,按照日期水平分表alarm_histrtory201912,alarm_histrtory202001,alarm_histrtory202002,分别代表2019年12月、2020年1月、2020年2月的数据表。
每个月都需要创建一张新的数据表,但是Sharding-JDBC水平分表不能动态变化,所以,为了实现Sharding-JDBC的水平分表配置随着时间,动态修改,而无需程序重启。
示例代码:https://github.com/xujingle1995/sharding-jdbc
解决方案:
方案一:
通过配置中心,修改配置文件,然后sharding-jdbc自动获取新的分库分表配置,从而实现动态修改。这个方案还是需要人的介入,如果需要了解这种方案,只需要springboot中引入nacos配置中心即可。如果nacos不会配置可以参考:
1.官方文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config
2.相关博客:https://blog.csdn.net/qq_26932225/article/details/86536837、https://blog.csdn.net/qq_26932225/article/details/86548829
方案二:
只需要以下三步:
1.自定义分片算法类
2.添加spring定时任务,动态修改Sharding-JDBC的配置。
3.配置application.properties配置文件
开始实践
1.准备工作:
创建Springboot工程,pom.xml引入mysql、mybatis、sharding-jdbc依赖
创建数据库:见本文最后的SQL语句
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.2
org.springframework.boot
spring-boot-devtools
runtime
true
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
4.0.0-RC1
2.自定义分片算法类(重要)
自定义分片算法类,用于当SQL语句中包含了分片键,sharding-jdbc会调用该类的doSharding方法,得到要查询的实际数据表名我这里自定义乐standard的精确分片和范围分片:
package com.xjl.sharding.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
/**
* @Title: dongfangdianqi
* @description: alarmHistory 精确分片 = in
* @create: 2020-02-25 14:12
* @update: 2020-02-25 14:12
* @updateRemark: 修改内容
* @Version: 1.0
*/
@Slf4j
public class PreciseSharingTableAlgorithmOfAlarmhis implements PreciseShardingAlgorithm {
private SimpleDateFormat dateformat = new SimpleDateFormat("yyyyMM");
@Override
public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
StringBuffer tableName = new StringBuffer();
log.info("执行操作的表名{}",shardingValue.getLogicTableName() + dateformat.format(shardingValue.getValue()));
tableName.append(shardingValue.getLogicTableName()).append(dateformat.format(shardingValue.getValue()));
return tableName.toString();
}
}
package com.xjl.sharding.config;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @Title: dongfangdianqi
* @description: 根据发生时间的范围查询分片算法 between and
* @create: 2020-02-25 16:55
* @update: 2020-02-25 16:55
* @updateRemark: 修改内容
* @Version: 1.0
*/
@Slf4j
public class RangeShardingAlgorithmOfAlarmhis implements RangeShardingAlgorithm {
private static SimpleDateFormat dateformat = new SimpleDateFormat("yyyyMM");
@Override
public Collection doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) {
Collection result = new LinkedHashSet<>();
Range shardingKey = shardingValue.getValueRange();
// 获取起始,终止时间范围
Date startTime = shardingKey.lowerEndpoint();
Date endTime = shardingKey.upperEndpoint();
Date now = new Date();
if (startTime.after(now)){
startTime = now;
}
if (endTime.after(now)){
endTime = now;
}
Collection tables = getRoutTable(shardingValue.getLogicTableName(), startTime, endTime);
if (tables != null && tables.size() >0) {
result.addAll(tables);
}
return result;
}
private Collection getRoutTable(String logicTableName, Date startTime, Date endTime) {
Set rouTables = new HashSet<>();
if (startTime != null && endTime != null) {
List rangeNameList = getRangeNameList(startTime, endTime);
for (String YearMonth : rangeNameList) {
rouTables.add(logicTableName + YearMonth);
}
}
return rouTables;
}
private static List getRangeNameList(Date startTime, Date endTime) {
List result = Lists.newArrayList();
// 定义日期实例
Calendar dd = Calendar.getInstance();
dd.setTime(startTime);
while(dd.getTime().before(endTime)) {
result.add(dateformat.format(dd.getTime()));
// 进行当前日期按月份 + 1
dd.add(Calendar.MONTH, 1);
}
return result;
}
}
3.添加定时任务类
package com.xjl.sharding.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @Description:
* @author: 许京乐
* @date: 2020/3/1 21:50
*/
@ConfigurationProperties(prefix = "dynamic.table")
@Data
public class DynamicTablesProperties {
String[] names;
}
package com.xjl.sharding.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.core.exception.ShardingConfigurationException;
import org.apache.shardingsphere.core.rule.DataNode;
import org.apache.shardingsphere.core.rule.TableRule;
import org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.ShardingDataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* @Description:水平分表,动态分表刷新定时任务
* @author: 许京乐
* @date: 2020/2/29 23:47
*/
@Component
@EnableScheduling
@EnableConfigurationProperties(DynamicTablesProperties.class)
@Slf4j
public class ShardingTableRuleActualTablesRefreshSchedule implements InitializingBean {
@Autowired
private DynamicTablesProperties dynamicTables;
@Autowired
private DataSource dataSource;
public ShardingTableRuleActualTablesRefreshSchedule() {
}
@Scheduled(cron = "0 0 0 * * *")
public void actualTablesRefresh() throws NoSuchFieldException, IllegalAccessException {
System.out.println("---------------------------------");
ShardingDataSource dataSource = (ShardingDataSource) this.dataSource;
if (dynamicTables.getNames() == null || dynamicTables.getNames().length == 0) {
log.warn("dynamic.table.names为空");
return;
}
for (int i = 0; i < dynamicTables.getNames().length; i++) {
TableRule tableRule = null;
try {
tableRule = dataSource.getShardingContext().getShardingRule().getTableRule(dynamicTables.getNames()[i]);
System.out.println(tableRule);
} catch (ShardingConfigurationException e) {
log.error("逻辑表:{},不存在配置!", dynamicTables.getNames()[i]);
}
List dataNodes = tableRule.getActualDataNodes();
Field actualDataNodesField = TableRule.class.getDeclaredField("actualDataNodes");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(actualDataNodesField, actualDataNodesField.getModifiers() & ~Modifier.FINAL);
actualDataNodesField.setAccessible(true);
// !!!!!!!!默认水平分表开始时间是2019-12月,每个月新建一张新表!!!!!
LocalDateTime localDateTime = LocalDateTime.of(2019, 12, 1, 0, 0, new Random().nextInt(59));
LocalDateTime now = LocalDateTime.now();
String dataSourceName = dataNodes.get(0).getDataSourceName();
String logicTableName = tableRule.getLogicTable();
StringBuilder stringBuilder = new StringBuilder(10).append(dataSourceName).append(".").append(logicTableName);
final int length = stringBuilder.length();
List newDataNodes = new ArrayList<>();
while (true) {
stringBuilder.setLength(length);
stringBuilder.append(localDateTime.format(DateTimeFormatter.ofPattern("yyyyMM")));
DataNode dataNode = new DataNode(stringBuilder.toString());
newDataNodes.add(dataNode);
localDateTime = localDateTime.plusMonths(1L);
if (localDateTime.isAfter(now)) {
break;
}
}
actualDataNodesField.set(tableRule, newDataNodes);
}
}
@Override
public void afterPropertiesSet() throws Exception {
actualTablesRefresh();
}
}
4.application.properties配置文件
# sharding-jdbc 相关配置
# 配置水平分表随着日期每月递增的逻辑表名,配置后不走分片建,全局查询时能够自动获取最新的逻辑表分片,多个通过逗号分隔
dynamic.table.names=alarmhis
# 数据源配置
spring.shardingsphere.datasource.names = ds0
spring.shardingsphere.datasource.ds0.type = com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver‐class‐name = com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url = jdbc:mysql://IP地址:端口号/dfdq?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
spring.shardingsphere.datasource.ds0.username = 你的数据库账户
spring.shardingsphere.datasource.ds0.password = 你的数据库密码
## 分表策略 其中alarmhis为逻辑表 分表主要取决与almhappentime字段
spring.shardingsphere.sharding.tables.alarmhis.actual-data-nodes=ds0.alarmhis
spring.shardingsphere.sharding.tables.alarmhis.table-strategy.standard.sharding-column=AlmClearTime
# 自定义分表算法
spring.shardingsphere.sharding.tables.alarmhis.table-strategy.standard.precise-algorithm-class-name=com.dfdq.common.sharding.jdbc.PreciseSharingTableAlgorithmOfAlarmhis
spring.shardingsphere.sharding.tables.alarmhis.table-strategy.standard.range-algorithm-class-name=com.dfdq.common.sharding.jdbc.RangeShardingAlgorithmOfAlarmhis
# 打印解析后的SQL语句
spring.shardingsphere.props.sql.show = true
# sharding jdbc 需要重新注入数据源,覆盖原本注入的数据源
spring.main.allow-bean-definition-overriding=true
5.创建实体类以及Dao层
package com.xjl.sharding.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.Instant;
/**
* @Description:
* @date: 2020/2/29 16:11
*/
@Data
public class AlarmHistoryDO {
private int turbineID;
private int almPointID;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Instant almHappenTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Instant almClearTime;
}
package com.xjl.sharding.dao;
import com.xjl.sharding.entity.AlarmHistoryDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.Instant;
import java.util.List;
/**
* @Description:历史报警信息表数据类
* @author: 许京乐
* @date: 2020/2/29 16:06
*/
@Mapper
public interface TestDao {
List getAlarmHistoryById(@Param("id") String id);
List getAlarmHistoryByTime(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
}
6.编写单元测试
测试中SQL语句没有走分片键,实际查询语句是全表查询,并且定时任务会动态修改实际水平分表
package com.xjl.sharding;
import com.xjl.sharding.dao.TestDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ShardingApplicationTests {
@Autowired
TestDao testDao;
@Test
public void contextLoads() {
testDao.getAlarmHistoryById("1");
}
}
7. 打印日志
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.13.RELEASE)
2020-03-12 12:30:16.129 INFO 88264 --- [ main] c.xjl.sharding.ShardingApplicationTests : Starting ShardingApplicationTests on LAPTOP-47DUEIIO with PID 88264 (started by xujingle in D:\ProjectCode\NewDuty\sharding)
2020-03-12 12:30:16.131 INFO 88264 --- [ main] c.xjl.sharding.ShardingApplicationTests : No active profile set, falling back to default profiles: default
2020-03-12 12:30:18.437 INFO 88264 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-03-12 12:30:18.965 INFO 88264 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
---------------------------------
TableRule(logicTable=alarmhis, actualDataNodes=[DataNode(dataSourceName=ds0, tableName=alarmhis201912), DataNode(dataSourceName=ds0, tableName=alarmhis202001)], databaseShardingStrategy=null, tableShardingStrategy=org.apache.shardingsphere.core.strategy.route.standard.StandardShardingStrategy@6d303498, generateKeyColumn=null, shardingKeyGenerator=null, logicIndex=null)
2020-03-12 12:30:26.022 INFO 88264 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-03-12 12:30:26.762 INFO 88264 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2020-03-12 12:30:26.893 INFO 88264 --- [ main] c.xjl.sharding.ShardingApplicationTests : Started ShardingApplicationTests in 11.374 seconds (JVM running for 12.764)
2020-03-12 12:30:27.990 INFO 88264 --- [ main] ShardingSphere-SQL : Rule Type: sharding
2020-03-12 12:30:27.990 INFO 88264 --- [ main] ShardingSphere-SQL : Logic SQL: SELECT * FROM alarmhis WHERE TurbineID = ?
2020-03-12 12:30:27.991 INFO 88264 --- [ main] ShardingSphere-SQL : SQLStatement: SelectStatement(super=DQLStatement(super=AbstractSQLStatement(type=DQL, tables=Tables(tables=[Table(name=alarmhis, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=alarmhis, quoteCharacter=NONE, schemaNameLength=0)], parametersIndex=1, logicSQL=SELECT * FROM alarmhis WHERE TurbineID = ?)), containStar=true, firstSelectItemStartIndex=7, selectListStopIndex=7, groupByLastIndex=0, items=[StarSelectItem(owner=Optional.absent())], groupByItems=[], orderByItems=[], limit=null, subqueryStatement=null, subqueryStatements=[], subqueryConditions=[])
2020-03-12 12:30:27.991 INFO 88264 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT * FROM alarmhis201912 WHERE TurbineID = ? ::: [1]
2020-03-12 12:30:27.991 INFO 88264 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT * FROM alarmhis202001 WHERE TurbineID = ? ::: [1]
2020-03-12 12:30:27.991 INFO 88264 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT * FROM alarmhis202002 WHERE TurbineID = ? ::: [1]
2020-03-12 12:30:27.991 INFO 88264 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT * FROM alarmhis202003 WHERE TurbineID = ? ::: [1]
8.注意的点:
com.xjl.sharding.config.ShardingTableRuleActualTablesRefreshSchedule定时任务类中,设置了默认起始分表时间是从2019-12月,每个月分表一次。
application.properties配置文件中,spring.shardingsphere.sharding.tables.alarmhis.actual-data-nodes=只需要等于逻辑表名并且dynamic.table.names也需要设置水平分表的逻辑表名,如果是有很多需要水平分表的逻辑表,用逗号分隔
创建数据库SQL语句
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for alarmhis201912
-- ----------------------------
DROP TABLE IF EXISTS `alarmhis201912`;
CREATE TABLE `alarmhis201912` (
`TurbineID` tinyint(0) UNSIGNED NOT NULL DEFAULT 0,
`AlmPointID` smallint(0) UNSIGNED NOT NULL,
`AlmHappenTime` datetime(0) NOT NULL,
`AlmClearTime` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`TurbineID`, `AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `i_almID`(`AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `almTime`(`AlmHappenTime`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for alarmhis202001
-- ----------------------------
DROP TABLE IF EXISTS `alarmhis202001`;
CREATE TABLE `alarmhis202001` (
`TurbineID` tinyint(0) UNSIGNED NOT NULL DEFAULT 0,
`AlmPointID` smallint(0) UNSIGNED NOT NULL,
`AlmHappenTime` datetime(0) NOT NULL,
`AlmClearTime` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`TurbineID`, `AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `i_almID`(`AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `almTime`(`AlmHappenTime`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for alarmhis202002
-- ----------------------------
DROP TABLE IF EXISTS `alarmhis202002`;
CREATE TABLE `alarmhis202002` (
`TurbineID` tinyint(0) UNSIGNED NOT NULL DEFAULT 0,
`AlmPointID` smallint(0) UNSIGNED NOT NULL,
`AlmHappenTime` datetime(0) NOT NULL,
`AlmClearTime` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`TurbineID`, `AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `i_almID`(`AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `almTime`(`AlmHappenTime`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for alarmhis202003
-- ----------------------------
DROP TABLE IF EXISTS `alarmhis202003`;
CREATE TABLE `alarmhis202003` (
`TurbineID` tinyint(0) UNSIGNED NOT NULL DEFAULT 0,
`AlmPointID` smallint(0) UNSIGNED NOT NULL,
`AlmHappenTime` datetime(0) NOT NULL,
`AlmClearTime` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`TurbineID`, `AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `i_almID`(`AlmPointID`, `AlmHappenTime`) USING BTREE,
INDEX `almTime`(`AlmHappenTime`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
shardingjdbc全局表_Sharding-JDBC动态分表实现相关推荐
- sharding-jdbc系列之按月动态分表(十二)
后续文章首发在个人博客,欢迎移驾我的个人博客浏览该文章 https://shared-code.com/article/79 欢迎关注公众号 回复 "分库分表" 获取分库分表dem ...
- Sharding-Jdbc实现读写分离、分库分表,妙!
点击关注公众号,实用技术文章及时了解 1.概览 ShardingSphere-Jdbc定位为轻量级Java框架,在Java的Jdbc层提供的额外服务.它使用客户端直连数据库,以jar包形式提供服务,可 ...
- SSM项目引入sharding JDBC进行分表
SSM项目引入sharding JDBC进行分表 注意点: 本次集成sharing-jdbc 4.1.1,由于各个版本差别比较大,配置方式差别也特别大,请根据官方文档进行配置! 官方配置路径:http ...
- oracle 分表设计,oracle 分库分表(sharding)
数据库Sharding的基本思想和切分策 关于垂直切分Vertical Sharding的粒度 数据库分库分表(sharding)系列(一) 拆分实施策略和示例演示 数据库分库分表(sharding) ...
- 如何分库分表,怎样分库分表,为什么要分库分表?
如何分库分表,怎样分库分表,为什么要分库分表? ◆ 数据库瓶颈 ◆分库分表 1. 水平分库 2. 水平分表 3. 垂直分库 4. 垂直分表 ◆ 分库分表工具 ◆ 分库分表带来的问题 ■ 事务一致性问题 ...
- 分库分表之_分库分表 + 复杂查询
前言 Github:https://github.com/HealerJean 博客:http://blog.healerjean.com 代码配置暂时和和分库分表之_分库分表相同.但是为了测试下面的 ...
- Mycat 源码修改-实现分表规则:按天分表和取摸分表查询
Mycat 源码修改-实现分表规则:按天和取摸功能.之前修改过源码,发现其实没什么高深的只需要自己耐心点,多花点时间去调试就可以做到了.通过调试,找到自己想要改的地方,这是关键的:在代码中表现为修改相 ...
- 分库分表原因,分库分表的方式,分库分表带来的问题
分库分表 1 为什么分库分表 随着平台的业务发展,数据可能会越来越多,甚至达到亿级.以MySQL为例,单库数据量在5000万以内性能比较好,超过阈值后性能会随着数据量的增大而明显降低.单表的数据量 ...
- mysql 分表 好处_分库分表浅谈
什么是分库分表 顾名思义,分库分表就是按照一定的规则,对原有的数据库和表进行拆分,把原本存储于一个库的数据分块存储到多个库上,把原本存储于一个表的数据分块存储到多个表上. 为什么需要分库分表 随着 ...
最新文章
- Solr部署如何启动
- 【数据中台】关于数据中台系统,需要了解哪些技术?
- js+jQuery获取全选并用ajax进行批量删除
- 【集合论】关系闭包 ( 关系闭包相关定理 )
- 第八周实践项目3 顺序串一些算法操作
- 【leetcode 简单】 第一百一十题 分发饼干
- Xcode11 后Appdelegate自定义UIWindow对象失败详解。
- Servlet3.0 jsp跳转到Servlet 出现404错误的路径设置方法
- (六)授权(下):自定义permission
- 鼠标停留在按钮上显示文字
- Java8中list转map方法
- win10摄像头打开后黑屏怎么回事?(驱动重新装了、注册表按照网上的方法也改过了、相机隐私设置也打开了,总之各种方法都尝试了还是打开黑屏)
- Python 构建 Random Forest 和 XGBoost
- python列重命名
- 什么是数据源?如何配置数据源?
- 《TCPIP网络编程(尹圣雨)》PDF+源代码+目录;文章最底下有链接
- 【人工智能】全球老外正跟你同步修仙!AI垂直文本翻译助力国产网文出海,规模将达300亿!...
- mysql 数据库 ui查询_mysql数据库查询语句
- 【GAL中的标注弹窗功能——Renpy系列1】
- COOX培训材料 — SCADA(1.Valve)
热门文章
- python编程培训多少钱-python培训一般多少钱?[python培训]
- 初学者python用哪个版本好-python用哪个版本好
- python程序在安卓上如何运行-在 android 上运行 python 的方法
- python代码块使用缩进表示-Python 为什么使用缩进来划分代码块?
- python画三维几何图-Python常见几何图形绘制
- python3.5怎么安装pip-python3.5版本安装pip3
- python怎么安装包-怎么在windows下安装python第三方包
- python中的二进制、八进制、十六进制的相互转换
- Google Colab使用详细教程
- java excel md5,excel表格数据md5加密-excel 怎么把文本转化成md5