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

设计一个简易版的数据库路由

  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

文章目录

  • 数据库路由
    • 需求设计
    • 方案设计
      • 基于HashMap实现
      • 基于Mybatis实现
        • Mybatis的工作原理
          • 构建会话工厂
          • 会话运行
            • Executor(执行器)
            • StatementHandler(数据库会话器)
            • ParameterHandler (参数处理器)
            • ResultSetHandler(结果处理器)
        • 说说Mybatis的插件运行原理,如何编写一个插件?
          • 插件的运行原理?
          • 如何编写一个插件?
        • MyBatis是如何进行分页的?分页插件的原理是什么?
          • MyBatis是如何分页的?
          • 分页插件的原理是什么?
    • 核心代码
      • 注解
      • 配置加载
        • 数据库连接源的加载
        • 配置加载
        • 策略使用
          • 基于HashMap
          • 基于Mybatis
    • 测试效果
      • 配置文件
      • 基于HashMap

数据库路由

代码链接:https://gitee.com/ni-hongsheng/db-router.git

需求设计

数据库的分库分表的实现算法其实有很多,比如大名鼎鼎的mychat等,都可以解决这个问题,但是他们存在的问题是太重了,这也是众多功能堆积起来的后果。如果从零到一实现数据库分库分表呢?那么传统的思路是什么,都能在什么层面上解决这个问题呢?不如自己来实现一个数据库分库分表的插件出来。

方案设计

当有了需求,需要考虑要在什么层面上实现数据库路由呢,实现分库分表呢?其本质又是什么呢?其本质举个例子:比如说插入一条数据,鬼知道要插入到哪个库那个表里面去,先不考虑任何可扩展的问题,怎么插入,传统的mychat会有取余,哈希等办法吧。对这是一个好办法,但是一定非要这样嘛,现在的开发基本上都是使用SpringBoot + Mybatis的开发吧,如果基于Mybatis来实现这个,是不是也是一个思路呢?所以实现的方案就包含了两种,一种是基于HashMap实现,一种是基于Mybatis实现。

基于HashMap实现

相信能看这篇文章的基本上都了解HashMap的,算得上是必须要熟悉的基础知识了,初始就16个位置的数组,当我们往HashMap中存储的时候,其为了尽可能的避免HashMap碰撞,使其分布的更加均匀,做了很多的工作,如果产生了碰撞,链表和红黑树的优化做的也很好,但是这个终归是备用方案,实际上其HashMap的Hash函数设计的非常的好,其本质上是Hash函数的前十六位与后十六位异或,然后在与(size-1)与。通过这样设计能尽可能的减少碰撞。所以基于HashMap的实现的核心就是将这套方案迁移进算法的实现中。

其中HashMap的基础知识可以参考这篇文章:如果面试也能这样说HashMap,那么就不会有那么多遗憾!-CSDN博客

基于Mybatis实现

关于Mybatis的分库分表的额实现比较复杂,得从Mybatis的工作原理说起

Mybatis的工作原理

我们已经大概知道了MyBatis的工作流程,按工作原理,可以分为两大步: 生成会话工厂会话运行

在这里插入图片描述

MyBatis是一个成熟的框架,篇幅限制,这里抓大放小,来看看它的主要工作流程。

构建会话工厂

构造会话工厂也可以分为两步:

在这里插入图片描述

  • 获取配置

获取配置这一步经过了几步转化,最终由生成了一个配置类Configuration实例,这个配置类实例非常重要,主要作用包括:

  1. 读取配置文件,包括基础配置文件和映射文件
  2. 初始化基础配置,比如MyBatis的别名,还有其它的一些重要的类对象,像插件、映射器、ObjectFactory等等
  3. 提供一个单例,作为会话工厂构建的重要参数
  4. 它的构建过程也会初始化一些环境变量,比如数据源
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {SqlSessionFactory var5;//省略异常处理//xml配置构建器XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);//通过转化的Configuration构建SqlSessionFactoryvar5 = this.build(parser.parse());
}
  • 构建SqlSessionFactory

SqlSessionFactory只是一个接口,构建出来的实际上是它的实现类的实例,一般我们用的都是它的实现类DefaultSqlSessionFactory

public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);
}
会话运行

会话运行是MyBatis最复杂的部分,它的运行离不开四大组件的配合:

Executor(执行器)

Executor起到了至关重要的作用,SqlSession只是一个门面,相当于客服,真正干活的是是Executor,就像是默默无闻的工程师。它提供了相应的查询和更新方法,以及事务方法。

	Environment environment = this.configuration.getEnvironment();TransactionFactory transactionFactory =
this.getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);//通过Configuration创建executorExecutor executor = this.configuration.newExecutor(tx, execType);var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
StatementHandler(数据库会话器)

StatementHandler,顾名思义,处理数据库会话的。我们以SimpleExecutor为例,看一下它的查询方法,先生成了一个StatementHandler实例,再拿这个handler去执行query。

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;List var9;try {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms,
parameter, rowBounds, resultHandler, boundSql);stmt = this.prepareStatement(handler,ms.getStatementLog());var9 = handler.query(stmt, resultHandler);} finally {this.closeStatement(stmt);}return var9;
}

再以最常用的PreparedStatementHandler看一下它的query方法,其实在上面的prepareStatement 已经对参数进行了预编译处理,到了这里,就直接执行sql,使用ResultHandler处理返回结果。

public <E> List<E> query(Statement statement,ResultHandler resultHandler) throws SQLException {PreparedStatement ps =(PreparedStatement)statement;ps.execute();return this.resultSetHandler.handleResultSets(ps);
}
ParameterHandler (参数处理器)

PreparedStatementHandler里对sql进行了预编译处理

public void parameterize(Statement statement) throws SQLException {this.parameterHandler.setParameters((PreparedStatement)statement);
}

这里用的就是ParameterHandler,setParameters的作用就是设置预编译SQL语句的参数。

里面还会用到typeHandler类型处理器,对类型进行处理。

public interface ParameterHandler {Object getParameterObject();void setParameters(PreparedStatement var1) throwsSQLException;
}
ResultSetHandler(结果处理器)

我们前面也看到了,最后的结果要通过ResultSetHandler来进行处理,handleResultSets这个方法就是用来包装结果集的。Mybatis为我们提供了一个DefaultResultSetHandler,通常都是用这个实现类去进行结果的处理的。

它会使用typeHandle处理类型,然后用ObjectFactory提供的规则组装对象,返回给调用者。

整体上总结一下会话运行:

在这里插入图片描述

我们最后把整个的工作流程串联起来,简单总结一下:

在这里插入图片描述

  1. 读取 MyBatis 配置文件——mybatis-config.xml 、加载映射文件——映射文件即SQL 映射文件,文件中配置了操作数据库的 SQL 语句。最后生成一个配置对象。
  2. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂SqlSessionFactory。
  3. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
  4. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
  5. StatementHandler:数据库会话器,串联起参数映射的处理和运行结果映射的处理。
  6. 参数处理:对输入参数的类型进行处理,并预编译。
  7. 结果处理:对返回结果的类型进行处理,根据对象映射规则,返回相应的对象。

讲了这么多Mybatis的工作原理,那么是怎么基于Mybatis实现分库分表的呢?说说Mybatis的插件运行原理,如何编写一个插件?

说说Mybatis的插件运行原理,如何编写一个插件?
插件的运行原理?

Mybatis会话的运行需要ParameterHandler、ResultSetHandler、StatementHandler、Executor这四大对象的配合,插件的原理就是在这四大对象调度的时候,插入一些我我们自己的代码。

在这里插入图片描述

Mybatis使用JDK的动态代理,为目标对象生成代理对象。它提供了一个工具类Plugin ,实现了 InvocationHandler 接口。

在这里插入图片描述

使用 Plugin 生成代理对象,代理对象在调用方法的时候,就会进入invoke方法,在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会在这里被我们调用,然后就返回结果。如果不存在签名方法,那么将直接反射调用我们要执行的方法。

如何编写一个插件?

我们自己编写MyBatis 插件,只需要实现拦截器接口 Interceptor (org.apache.ibatis.plugin Interceptor ),在实现类中对拦截对象和方法进行处理。

实现Mybatis的Interceptor接口并重写intercept()方法

public class MyInterceptor implements Interceptor {Properties props=null;@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("before……");//如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法// 如果不是它就会调用下个插件代理对象的invoke方法Object obj=invocation.proceed();System.out.println("after……");return obj;}
}

然后再给插件编写注解,确定要拦截的对象,要拦截的方法

@Intercepts({@Signature(type = Executor.class, //确定要拦截的对象method = "update", //确定要拦截的方法args = {MappedStatement.class,Object.class} //拦截方法的参数
)})
public class MyInterceptor implements Interceptor {Properties props=null;@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("before……");//如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法// 如果不是它就会调用下个插件代理对象的invoke方法Object obj=invocation.proceed();System.out.println("after……");return obj;}
}

最后,再MyBatis配置文件里面配置插件

<plugins><plugin interceptor="xxx.MyPlugin"><property name="dbType",value="mysql"/></plugin>
</plugins>
MyBatis是如何进行分页的?分页插件的原理是什么?
MyBatis是如何分页的?

MyBatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的原理是什么?
  • 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,拦截Executor的query方法
  • 在执行查询的时候,拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
  • 举例:select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10

核心代码

注解

@Documented // 元注解表示该注解应该包含在生成的API文档中,以便开发者能够看到并了解它。
@Retention(RetentionPolicy.RUNTIME) // 元注解表示该注解的生命周期将保留到运行时,也就是说,在运行时可以通过反射机制获取并使用该注解。
@Target({ElementType.TYPE, ElementType.METHOD}) // 元注解表示该注解可以应用于类和方法上。
public @interface DBRouter {String key() default "";}// 路由策略,分表标记@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouterStrategy {boolean splitTable() default false;}

配置加载

数据库连接源的加载
@Beanpublic DataSource dataSource() {// 创建数据源Map<Object, Object> targetDataSources = new HashMap<>();for (String dbInfo : dataSourceMap.keySet()) {Map<String, Object> objMap = dataSourceMap.get(dbInfo);targetDataSources.put(dbInfo, new DriverManagerDataSource(objMap.get("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));}// 设置动态数据源DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setTargetDataSources(targetDataSources);// 设置默认数据源dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(), defaultDataSourceConfig.get("username").toString(), defaultDataSourceConfig.get("password").toString()));return dynamicDataSource;}// 动态数据源的事务管理@Beanpublic TransactionTemplate transactionTemplate(DataSource dataSource) {DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource);TransactionTemplate transactionTemplate = new TransactionTemplate();transactionTemplate.setTransactionManager(dataSourceTransactionManager);transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRED");return transactionTemplate;}

以上代码动态的配置了数据库的连接 和 事务

配置加载
public class DataSourceAutoConfig implements EnvironmentAware {......// 设置数据源,将数据源注入到属性当中@Overridepublic void setEnvironment(Environment environment) {String prefix = "mini-db-router.jdbc.datasource.";dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));routerKey = environment.getProperty(prefix + "routerKey");// 分库分表数据源String dataSources = environment.getProperty(prefix + "list");assert dataSources != null;for (String dbInfo : dataSources.split(",")) {Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);dataSourceMap.put(dbInfo, dataSourceProps);}// 默认数据源String defaultData = environment.getProperty(prefix + "default");defaultDataSourceConfig = PropertyUtil.handle(environment, prefix + defaultData, Map.class);}
策略使用
基于HashMap
// 注入IDBRouterStrategy// 在这里使用策略模式额外封装了一层,这样可以动态适配多个路由算法@Beanpublic IDBRouterStrategy dbRouterStrategy(DBRouterConfig dbRouterConfig) {return new DBRouterStrategyHashCode(dbRouterConfig);}public class DBRouterStrategyHashCode implements IDBRouterStrategy {private Logger logger = LoggerFactory.getLogger(DBRouterStrategyHashCode.class);private DBRouterConfig dbRouterConfig;public DBRouterStrategyHashCode(DBRouterConfig dbRouterConfig) {this.dbRouterConfig = dbRouterConfig;}@Overridepublic void doRouter(String dbKeyAttr) {int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();// 扰动函数;在 JDK 的 HashMap 中,对于一个元素的存放,需要进行哈希散列。而为了让散列更加均匀,// 所以添加了扰动函数。int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));// 库表索引;相当于是把一个长条的桶,切割成段,对应分库分表中的库编号和表编号// 公式目的;8个位置,计算出来的是位置在5 那么你怎么知道5是在2库1表。int dbIdx = idx / dbRouterConfig.getTbCount() + 1;int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);// 设置到 ThreadLocalDBContextHolder.setDBKey(String.format("%02d", dbIdx));DBContextHolder.setTBKey(String.format("%03d", tbIdx));logger.debug("数据库路由 dbIdx:{} tbIdx:{}",  dbIdx, tbIdx);}@Overridepublic void setDBKey(int dbIdx) {DBContextHolder.setDBKey(String.format("%02d", dbIdx));}@Overridepublic void setTBKey(int tbIdx) {DBContextHolder.setTBKey(String.format("%03d", tbIdx));}@Overridepublic int dbCount() {return dbRouterConfig.getDbCount();}@Overridepublic int tbCount() {return dbRouterConfig.getTbCount();}@Overridepublic void clear(){DBContextHolder.clearDBKey();DBContextHolder.clearTBKey();}}

其本质也如注释一般,计算出了扰动因子,然后通过扰动因子动态的计算数据库和表。

基于Mybatis
@Beanpublic Interceptor plugin() {return new DynamicMybatisPlugin();}/**这个和mybatis的执行过程有关*/// 第一行标注了该拦截器需要拦截的方法,即prepare方法,
// 该方法在StatementHandler对象上执行。StatementHandler是MyBatis中用于处理预编译的SQL语句的接口。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DynamicMybatisPlugin implements Interceptor {// 使用正则表达式将SQL语句中的表名提取出来。正则表达式的模式为匹配以"from"、"into"或"update"开头的单词,// 然后紧跟一个或多个空格,再紧跟一个或多个非空字符(即表名)。private Pattern pattern = Pattern.compile("(from|into|update)[\\s]{1,}(\\w{1,})", Pattern.CASE_INSENSITIVE);@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 在intercept方法中,首先获取到被拦截的StatementHandler对象和相关的元数据信息。// 获取StatementHandlerStatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");// 获取自定义注解判断是否进行分表操作// 通过反射获取被拦截的方法对应的类,然后判断该类是否使用了自定义注解DBRouterStrategy,// 并且该注解的splitTable属性为true。如果没有使用该注解或者splitTable属性为false,则直接返回,不进行分表操作。String id = mappedStatement.getId();String className = id.substring(0, id.lastIndexOf("."));Class<?> clazz = Class.forName(className);DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);// 如果使用了DBRouterStrategy注解并且splitTable属性为true,则获取当前SQL语句。if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){return invocation.proceed();}// 获取SQLBoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();// 替换SQL表名 USER 为 USER_03// 使用正则表达式将SQL语句中的表名提取出来。正则表达式的模式为匹配以"from"、"into"或"update"开头的单词,// 然后紧跟一个或多个空格,再紧跟一个或多个非空字符(即表名)。// 使用正则表达式替换原始SQL语句中的表名为新的表名。Matcher matcher = pattern.matcher(sql);String tableName = null;if (matcher.find()) {tableName = matcher.group().trim();}assert null != tableName;// 将匹配到的表名与分表键值拼接,生成新的表名。String replaceSql = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());// 通过反射修改SQL语句// 使用反射将修改后的SQL语句设置回BoundSql对象中。Field field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);field.set(boundSql, replaceSql);field.setAccessible(false);// 最后调用invocation.proceed()方法继续执行原始的数据库操作。return invocation.proceed();}
// 该拦截器主要用于在满足特定条件时对SQL进行修改,实现动态分表的功能。
// 通过自定义注解DBRouterStrategy和正则表达式匹配,提取表名并进行替换,从而实现对特定表名的分表操作。
}

测试效果

配置文件

# 路由配置
router:jdbc:datasource:dbCount: 2tbCount: 4list: db01,db02db01:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/bugstack_01?useUnicode=trueusername: rootpassword: 123456db02:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/bugstack_02?useUnicode=trueusername: rootpassword: 123456

基于HashMap

<select id="queryUserInfoByUserId" parameterType="cn.nhs.test.infrastructure.po.User"resultType="cn.nhs.test.infrastructure.po.User">SELECT id, userId, userNickName, userHead, userPassword, createTimeFROM user_${tbIdx}where userId = #{userId}</select>
@Mapper
public interface IUserDao {@DBRouter(key = "userId")User queryUserInfoByUserId(User req);@DBRouter(key = "userId")void insertUser(User req);}

通过这样的路由计算就可以动态的插入到对应的库和表中,而基于Mybatis的更加方便,不需要修改mapper.xml文件即可实现。

相关文章:

设计一个简易版的数据库路由

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术&#x1f525;如果感觉博主的文章还不错的…...

接口自动化测试面试题

前言 前面总结了一篇关于接口测试的常规面试题&#xff0c;现在接口自动化测试用的比较多&#xff0c;也是被很多公司看好。那么想做接口自动化测试需要具备哪些能力呢&#xff1f; 也就是面试的过程中&#xff0c;面试官会考哪些问题&#xff0c;知道你是不是真的做过接口自动…...

Tampermonkey油猴插件-各大网盘批量分享,解放双手-上

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列...

【DB2】installSAM执行后会重启这件事

碎碎念 在使用自动化工具安装TSAMP的过程中&#xff0c;机器会自动重启这件事。 TSAMP真的挺折磨的&#xff0c;一个月居然因为这件事情debug两次了。 在测试自动化脚本的时候&#xff0c;第一遍安装都是好好的&#xff0c;从第二遍开始&#xff08;因为要测试脚本的幂等性&…...

RTSP网络视频协议

一.RTSP网络视频协议介绍 RTSP是类似HTTP的应用层协议&#xff0c;一个典型的流媒体框架网络体系可参考下图&#xff0c;其中rtsp主要用于控制命令&#xff0c;rtcp主要用于视频质量的反馈&#xff0c;rtp用于视频、音频流从传输。 1、RTSP&#xff08;Real Time Streaming P…...

Python 网络数据采集(四):Selenium 自动化

Python 网络数据采集&#xff08;四&#xff09;&#xff1a;Selenium 自动化 前言一、背景知识Selenium 4Selenium WebDriver 二、Selenium WebDriver 的安装与配置2.1 下载 Chrome 浏览器的驱动程序2.2 配置环境变量三、Python 安装 Selenium四、页面元素定位4.1 选择浏览器开…...

实现秒杀功能设计

页面 登录页面 登录成功后&#xff0c;跳转商品列表 商品列表页 加载商品信息 商品详情页 根据商品id查出商品信息返回VO&#xff08;包括rmiaoshaStatus、emainSeconds&#xff09;前端根据数据展示秒杀按钮&#xff0c;点击开始秒杀 订单详情页 秒杀页面设置 后端返回秒杀…...

每天刷两道题——第十四天

1.1矩阵置零 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用原地算法。 输入&#xff1a;matrix [[0,1,2,0],[3,4,5,2],[1,3,1,5]] 输出&#xff1a;[[0,0,0,0],[0,4,5,0],[0,3,1,0]] 原地算法&#xff08;…...

快速掌握Postman实现接口测试

快速掌握Postman实现接口测试 Postman简介 Postman是谷歌开发的一款网页调试和接口测试工具&#xff0c;能够发送任何类型的http请求&#xff0c;支持GET/PUT/POST/DELETE等方法。Postman非常简单易用&#xff0c;可以直接填写URL&#xff0c;header&#xff0c;body等就可以发…...

jmeter--3.使用提取器进行接口关联

目录 1. 正则表达式提取器 1.1 提取单个数据 1.2 名词解释 1.3 提取多个数据 2. 边界值提取器 2.2 名词解释 3. JSON提取器 3.1 Json语法 3.2 名词解释 3.3 如果有多组数据&#xff0c;同正则方式引用数据 1. 正则表达式提取器 示例数据&#xff1a;{"access_to…...

移动通信系统关键技术多址接入MIMO学习(8)

1.Multiple-antenna Techniques多天线技术MIMO&#xff0c;从SISO到SIMO到MISO到如今的MIMO&#xff1b; 2.SIMO单发多收&#xff0c;分为选择合并、增益合并&#xff1b;SIMO&#xff0c;基站通过两路路径将信号发送到终端&#xff0c;因为终端接收到的两路信号都是来自同一天…...

WorkPlus AI助理为企业提供智能客服的机器人解决方案

在数字化时代&#xff0c;企业面临着客户服务的重要挑战。AI客服机器人成为了提升客户体验和提高工作效率的关键工具。作为一款优秀的AI助理&#xff0c;WorkPlus AI助理以其智能化的特点和卓越的功能&#xff0c;为企业提供了全新的客服机器人解决方案。 为什么选择WorkPlus A…...

python类装饰器编写单体类

1 python类装饰器编写单体类 类装饰器用于装饰类&#xff0c;用于管理类自身&#xff0c;或用于管理实例创建调用。 单体类&#xff0c;不管创建多少次实例&#xff0c;都只有一个实例的类。可以通过类装饰器管理装饰类的全部实例&#xff0c;实现单体类。 1.1 字典存放单体…...

Java并发Condition 详解

1.引言 在Java并发编程中&#xff0c;线程间的协作是一个核心话题。为了实现线程间的协作&#xff0c;Java提供了多种机制&#xff0c;其中等待/通知机制是最常见的一种。在早期版本中&#xff0c;我们通过Object类提供的wait、notify和notifyAll方法来实现这种机制。然而&…...

如何使用CentOS系统中的Apache服务器提供静态HTTP服务

在CentOS系统中&#xff0c;Apache服务器是一个常用的Web服务器软件&#xff0c;它可以高效地提供静态HTTP服务。以下是在CentOS中使用Apache提供静态HTTP服务的步骤&#xff1a; 1. 安装Apache服务器 首先&#xff0c;您需要确保已安装Apache服务器。可以使用以下命令安装Ap…...

Python入门0基础学习笔记

1.编程之前 在编写代码之前&#xff0c;还有两件事需要做&#xff1a; 安装 Python 解释器&#xff1a;计算机是没法直接读懂 Python 代码的&#xff0c;需要一个解释器作为中间的翻译&#xff0c;把代码转换成字节码之后再执行。 Python 是翻译一行执行一行。一般说的安装 …...

python绘制热力图-数据处理-VOC数据类别标签分布及数量统计(附代码)

前言 当你需要统计训练数据中每个类别标签有多少&#xff0c;并且想知道坐标中心分布在图像的位置信息时&#xff0c;你可以利用一下脚本进行计算&#xff01; 步骤 要绘制热力图来分析VOC数据的分布统计&#xff0c;可以按照以下步骤进行&#xff1a; 数据处理&#xff1…...

【回顾2023,展望2024】砥砺前行

2023年总结 转眼间&#xff0c;迎来了新的一年2024年&#xff0c;回顾2023&#xff0c;对于我来说是一个充满平凡但又充实又幸运的一年。这一年经历了很多的事情&#xff0c;包括博客创作、技术学习、出书、买房等&#xff0c;基本上每件事情都是一个前所未有的挑战和机遇、使…...

Stable Diffusion初体验

体验了下 Stable Diffusion 2.0 的图片生成&#xff0c;效果还是挺惊艳的&#xff0c;没有细调prompt输入&#xff0c;直接输入了下面的内容&#xff1a; generate a Elimination Game image of burnning tree, Cyberpunk style 然后点击生成&#xff0c;经过了10多秒的等待就输…...

缓存解析:从架构设计到Redis应用及最佳实践

典型架构设计中缓存的存储位置 在现代软件架构中&#xff0c;缓存是优化数据检索、提高应用性能的关键组件。缓存的存储位置多种多样&#xff0c;每个位置针对特定的优化目标和需求。理解这些层级对于设计高效的系统至关重要。 浏览器缓存&#xff1a;这是最接近用户端的缓存层…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行&#xff0c;YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID&#xff1a; YW3…...

【生成模型】视频生成论文调研

工作清单 上游应用方向&#xff1a;控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...

JS设计模式(4):观察者模式

JS设计模式(4):观察者模式 一、引入 在开发中&#xff0c;我们经常会遇到这样的场景&#xff1a;一个对象的状态变化需要自动通知其他对象&#xff0c;比如&#xff1a; 电商平台中&#xff0c;商品库存变化时需要通知所有订阅该商品的用户&#xff1b;新闻网站中&#xff0…...

Go 语言并发编程基础:无缓冲与有缓冲通道

在上一章节中&#xff0c;我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道&#xff0c;它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好&#xff0…...

uniapp 开发ios, xcode 提交app store connect 和 testflight内测

uniapp 中配置 配置manifest 文档&#xff1a;manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号&#xff1a;4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...

wpf在image控件上快速显示内存图像

wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像&#xff08;比如分辨率3000*3000的图像&#xff09;的办法&#xff0c;尤其是想把内存中的裸数据&#xff08;只有图像的数据&#xff0c;不包…...

Android写一个捕获全局异常的工具类

项目开发和实际运行过程中难免会遇到异常发生&#xff0c;系统提供了一个可以捕获全局异常的工具Uncaughtexceptionhandler&#xff0c;它是Thread的子类&#xff08;就是package java.lang;里线程的Thread&#xff09;。本文将利用它将设备信息、报错信息以及错误的发生时间都…...

数据分析六部曲?

引言 上一章我们说到了数据分析六部曲&#xff0c;何谓六部曲呢&#xff1f; 其实啊&#xff0c;数据分析没那么难&#xff0c;只要掌握了下面这六个步骤&#xff0c;也就是数据分析六部曲&#xff0c;就算你是个啥都不懂的小白&#xff0c;也能慢慢上手做数据分析啦。 第一…...

标注工具核心架构分析——主窗口的图像显示

&#x1f3d7;️ 标注工具核心架构分析 &#x1f4cb; 系统概述 主要有两个核心类&#xff0c;采用经典的 Scene-View 架构模式&#xff1a; &#x1f3af; 核心类结构 1. AnnotationScene (QGraphicsScene子类) 主要负责标注场景的管理和交互 &#x1f527; 关键函数&…...