Spring Boot 配置主从数据库实现读写分离

一、前言

现在的 Web 应用大都是读多写少。除了缓存以外还可以通过数据库 “主从复制” 架构,把读请求路由到从数据库节点上,实现读写分离,从而大大提高应用的吞吐量。

通常,我们在 Spring Boot 中只会用到一个数据源,即通过 spring.datasource 进行配置。前文 《在 Spring Boot 中配置和使用多个数据源》 介绍了一种在 Spring Boot 中定义、使用多个数据源的方式。但是这种方式对于实现 “读写分离” 的场景不太适合。首先,多个数据源都是通过 @Bean 定义的,当需要新增额外的从数据库时需要改动代码,非常不够灵活。其次,在业务层中,如果需要根据读、写场景切换不同数据源的话只能手动进行。

对于 Spring Boot “读写分离” 架构下的的多数据源,我们需要实现如下需求:

  1. 可以通过配置文件新增数据库(从库),而不不需要修改代码。
  2. 自动根据场景切换读、写数据源,对业务层是透明的。

幸运的是,Spring Jdbc 模块类提供了一个 AbstractRoutingDataSource 抽象类可以实现我们的需求。

它本身也实现了 DataSource 接口,表示一个 “可路由” 的数据源。

核心的代码如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {// 维护的所有数据源@Nullableprivate Map<Object, DataSource> resolvedDataSources;// 默认的数据源@Nullableprivate DataSource resolvedDefaultDataSource;// 获取 Jdbc 连接@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return determineTargetDataSource().getConnection(username, password);}// 获取目标数据源protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");// 调用  determineCurrentLookupKey() 抽象方法,获取 resolvedDataSources 中定义的 key。Object lookupKey = determineCurrentLookupKey();DataSource dataSource = this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");}return dataSource;}// 抽象方法,返回 resolvedDataSources 中定义的 key。需要自己实现@Nullableprotected abstract Object determineCurrentLookupKey();
}

核心代码如上,它的工作原理一目了然。它在内部维护了一个 Map<Object, DataSource> 属性,维护了多个数据源。

当尝试从 AbstractRoutingDataSource 数据源获取数据源连接对象 Connection 时,会调用 determineCurrentLookupKey() 方法得到一个 Key,然后从数据源 Map<Object, DataSource> 中获取到真正的目标数据源,如果 Key 或者是目标数据源为 null 则使用默认的数据源。

得到目标数据数据源后,返回真正的 Jdbc 连接。这一切对于使用到 Jdbc 的组件(Repository、JdbcTemplate 等)来说都是透明的。

了解了 AbstractRoutingDataSource 后,我们来看看如何使用它来实现 “读写分离”。

二、实现思路

首先,创建自己的 AbstractRoutingDataSource 实现类。把它的默认数据源 resolvedDefaultDataSource 设置为主库,从库则保存到 Map<Object, DataSource> resolvedDataSources 中。

在 Spring Boot 应用中通常使用 @Transactional 注解来开启声明式事务,它的默认传播级别为 REQUIRED,也就是保证多个事务方法之间的相互调用都是在同一个事务中,使用的是同一个 Jdbc 连接。它还有一个 readOnly 属性表示是否是只读事务。

于是,我们可以通过 AOP 技术,在事务方法执行之前,先获取到方法上的 @Transactional 注解从而判断是读、还是写业务。并且把 “读写状态” 存储到线程上下文(ThreadLocal)中!

在 AbstractRoutingDataSource 的 determineCurrentLookupKey 方法中,我们就可以根据当前线程上下文中的 “读写状态” 判断当前是否是只读业务,如果是,则返回从库 resolvedDataSources 中的 Key,反之则返回 null 表示使用默认数据源也就是主库。

三、初始化数据库

首先,在本地创建 4 个不同名称的数据库,用于模拟 “MYSQL 主从” 架构。

-- 主库
CREATE DATABASE `demo_master` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
-- 从库
CREATE DATABASE `demo_slave1` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
-- 从库
CREATE DATABASE `demo_slave2` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
-- 从库
CREATE DATABASE `demo_slave3` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';

如上,创建了 4 个数据库。1 个主库,3 个从库。它们本质上毫无关系,并不是真正意义上的主从架构,这里只是为了方便演示。

接着,在这 4 个数据库下依次执行如下 SQL 创建一张名为 test 的表。

该表只有 2 个字段,1 个是 id 表示主键,一个是 name 表示名称。

CREATE TABLE `test` (`id` int NOT NULL COMMENT 'ID',`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

最后,初始化数据。往不同的数据库插入对应的记录。

INSERT INTO `demo_master`.`test` (`id`, `name`) VALUES (1, 'master');
INSERT INTO `demo_slave1`.`test` (`id`, `name`) VALUES (1, 'slave1');
INSERT INTO `demo_slave2`.`test` (`id`, `name`) VALUES (1, 'slave2');
INSERT INTO `demo_slave3`.`test` (`id`, `name`) VALUES (1, 'slave3');

不同数据库节点下 test 表中的 name 字段不同,用于区别不同的数据库节点。

四、创建应用

创建 Spring Boot 应用,添加 spring-boot-starter-jdbc 和 mysql-connector-j (MYSQL 驱动)依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId>
</dependency>

五、配置定义

我们需要在 application.yaml 中定义上面创建好的所有主、从数据库。

app:datasource:master: # 唯一主库jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=trueusername: rootpassword: rootslave: # 多个从库slave1:jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_slave1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=trueusername: rootpassword: rootslave2:jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_slave2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=trueusername: rootpassword: rootslave3:jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_slave3?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=trueusername: rootpassword: root

在 app.datasource.master 下配置了唯一的一个主库,也就是写库。然后在 app.datasource.slave 下以 Map 形式配置了多个从库(也就是读库),每个从库使用自定义的名称作为 Key。

数据源的实现使用的是默认的 HikariDataSource,并且数据源的配置是按照 HikariConfig 类定义的。也就是说,你可以根据 HikariConfig 的属性在配置中添加额外的设置。

有了配置后,还需要定义对应的配置类,如下:

package cn.springdoc.demo.db;import java.util.Map;
import java.util.Objects;
import java.util.Properties;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;@ConfigurationProperties(prefix = "app.datasource")  //  配置前缀
public class MasterSlaveDataSourceProperties {// 主库private final Properties master;// 从库private final Map<String, Properties> slave;@ConstructorBinding // 通过构造函数注入配置文件中的值public MasterSlaveDataSourceProperties(Properties master, Map<String, Properties> slave) {super();Objects.requireNonNull(master);Objects.requireNonNull(slave);this.master = master;this.slave = slave;}public Properties master() {return master;}public Map<String, Properties> slave() {return slave;}
}

还需要在 main 类上使用 @EnableConfigurationProperties 注解来加载我们的配置类:

package cn.springdoc.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.EnableAspectJAutoProxy;import cn.springdoc.demo.db.MasterSlaveDataSourceProperties;@SpringBootApplication
@EnableAspectJAutoProxy
@EnableConfigurationProperties(value = {MasterSlaveDataSourceProperties.class}) // 指定要加载的配置类
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}

这里还使用 @EnableAspectJAutoProxy 开启了 AOP 的支持,后面会用到。

六、创建 MasterSlaveDataSourceMarker

创建一个 MasterSlaveDataSourceMarker 类,用于维护当前业务的 “读写状态”。

package cn.springdoc.demo.db;public class MasterSlaveDataSourceMarker {private static final ThreadLocal<Boolean> flag = new ThreadLocal<Boolean>();// 返回标记public static Boolean get() {return flag.get();}// 写状态,标记为主库public static void master() {flag.set(Boolean.TRUE);}// 读状态,标记为从库public static void slave() {flag.set(Boolean.FALSE);}// 清空标记public static void clean() {flag.remove();}
}

通过 ThreadLocal<Boolean> 在当前线程中保存当前业务的读写状态。

如果 get() 返回 null 或者 true 则表示非只读,需要使用主库。反之则表示只读业务,使用从库。

七、创建 MasterSlaveDataSourceAop

创建 MasterSlaveDataSourceAop 切面类,在事务方法开始之前执行。

package cn.springdoc.demo.db;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)  // 在事务开始之前执行
public class MasterSlaveDataSourceAop {static final Logger log = LoggerFactory.getLogger(MasterSlaveDataSourceAop.class);@Pointcut(value = "@annotation(org.springframework.transaction.annotation.Transactional)")public void txMethod () {}@Around("txMethod()")public Object handle (ProceedingJoinPoint joinPoint) throws Throwable {// 获取当前请求的主从标识try {// 获取事务方法上的注解Transactional transactional = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(Transactional.class);if (transactional != null && transactional.readOnly()) {log.info("标记为从库");MasterSlaveDataSourceMarker.slave();    // 只读,从库} else {log.info("标记为主库");MasterSlaveDataSourceMarker.master(); // 可写,主库}// 执行业务方法Object ret = joinPoint.proceed();return ret;} catch (Throwable e) {throw e;} finally {MasterSlaveDataSourceMarker.clean();}}
}

首先,通过 @Order(Ordered.HIGHEST_PRECEDENCE) 注解保证它必须比声明式事务 AOP 更先执行。

该 AOP 会拦截所有声明了 @Transactional 的方法,在执行前从该注解获取 readOnly 属性从而判断是否是只读业务,并且在 MasterSlaveDataSourceMarker 标记。

八、创建 MasterSlaveDataSource

现在,创建 AbstractRoutingDataSource 的实现类 MasterSlaveDataSource:

package cn.springdoc.demo.db;import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class MasterSlaveDataSource extends AbstractRoutingDataSource {static final Logger log = LoggerFactory.getLogger(MasterSlaveDataSource.class);// 从库的 Key 列表private List<Object> slaveKeys;// 从库 key 列表的索引private AtomicInteger index = new AtomicInteger(0);@Overrideprotected Object determineCurrentLookupKey() {// 当前线程的主从标识Boolean master = MasterSlaveDataSourceMarker.get();if (master == null || master || this.slaveKeys.isEmpty()) {// 主库,返回 null,使用默认数据源log.info("数据库路由:主库");return null;}// 从库,从 slaveKeys 中选择一个 Keyint index = this.index.getAndIncrement() % this.slaveKeys.size();if (this.index.get() > 9999999) {this.index.set(0); }Object key = slaveKeys.get(index);log.info("数据库路由:从库 = {}", key);return key;}public List<Object> getSlaveKeys() {return slaveKeys;}public void setSlaveKeys(List<Object> slaveKeys) {this.slaveKeys = slaveKeys;}
}

其中,定义了一个 List<Object> slaveKeys 字段,用于存储在配置文件中定义的所有从库的 Key。

在 determineCurrentLookupKey 方法中,判断当前业务的 “读写状态”,如果是只读则通过 AtomicInteger 原子类自增后从 slaveKeys 轮询出一个从库的 Key。反之则返回 null 使用主库。

九、创建 MasterSlaveDataSourceConfiguration 配置类

最后,需要在 @Configuration 配置类中,创建 MasterSlaveDataSource 数据源 Bean。

package cn.springdoc.demo.db;import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;import javax.sql.DataSource;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;@Configuration
public class MasterSlaveDataSourceConfiguration {@Beanpublic DataSource dataSource(MasterSlaveDataSourceProperties properties) {MasterSlaveDataSource dataSource = new MasterSlaveDataSource();// 主数据库dataSource.setDefaultTargetDataSource(new HikariDataSource(new HikariConfig(properties.master())));// 从数据库Map<Object, Object> slaveDataSource = new HashMap<>();// 从数据库 KeydataSource.setSlaveKeys(new ArrayList<>());for (Map.Entry<String,Properties> entry : properties.slave().entrySet()) {if (slaveDataSource.containsKey(entry.getKey())) {throw new IllegalArgumentException("存在同名的从数据库定义:" + entry.getKey());}slaveDataSource.put(entry.getKey(), new HikariDataSource(new HikariConfig(entry.getValue())));dataSource.getSlaveKeys().add(entry.getKey());}// 设置从库dataSource.setTargetDataSources(slaveDataSource);return dataSource;}
}

首先,通过配置方法注入配置类,该类定义了配置文件中的主库、从库属性。

使用 HikariDataSource 实例化唯一主库数据源、和多个从库数据源,并且设置到 MasterSlaveDataSource 对应的属性中。

同时还存储每个从库的 Key,且该 Key 不允许重复。

十、测试

1、创建 TestService

创建用于测试的业务类。

package cn.springdoc.demo.service;import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class TestService {final JdbcTemplate jdbcTemplate;public TestService(JdbcTemplate jdbcTemplate) {super();this.jdbcTemplate = jdbcTemplate;}// 只读@Transactional(readOnly = true)public String read () {return this.jdbcTemplate.queryForObject("SELECT `name` FROM `test` WHERE id = 1;", String.class);} // 先读,再写@Transactionalpublic String write () {this.jdbcTemplate.update("UPDATE `test` SET `name` = ? WHERE id = 1;", "new name");return this.read();}
}

通过构造函数注入 JdbcTemplate(spring jdbc 模块自动配置的)。

Service 类中定义了 2 个方法。

  • read():只读业务,从表中检索 name 字段返回。
  • write:可写业务,先修改表中的 name 字段值为: new name,然后再调用 read() 方法读取修改后的结果、返回。

2、创建测试类

创建测试类,如下:

package cn.springdoc.demo.test;import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;import cn.springdoc.demo.service.TestService;@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {static final Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);@AutowiredTestService testService;@Testpublic void test() throws Exception {// 连续4次读log.info("read={}", this.testService.read());log.info("read={}", this.testService.read());log.info("read={}", this.testService.read());log.info("read={}", this.testService.read());// 写log.info("write={}", this.testService.write());}
}

在测试类方法中,连续调用 4 次 TestService 的 read() 方法。由于这是一个只读方法,按照我们的设定,它会在 3 个从库之间轮询使用。由于我们故意把三个从库 test 表中 name 的字段值设置得不一样,所以这里可以通过返回的结果看出来是否符合我们的预期。

最后调用了一次 write() 方法,按照设定会路由到主库。先 UPDATE 修改数据,再调用 read() 读取数据,虽然 read() 设置了 @Transactional(readOnly = true),但因为入口方法是 write(),所以 read() 还是会从主库读取数据(默认的事务传播级别)。

执行测试,输出的日志如下:

[           main] c.s.demo.db.MasterSlaveDataSourceAop     : 标记为从库
[           main] c.s.demo.db.MasterSlaveDataSource        : 数据库路由:从库 = slave1
[           main] c.s.demo.test.DemoApplicationTests       : read=slave1
[           main] c.s.demo.db.MasterSlaveDataSourceAop     : 标记为从库
[           main] c.s.demo.db.MasterSlaveDataSource        : 数据库路由:从库 = slave2
[           main] c.s.demo.test.DemoApplicationTests       : read=slave2
[           main] c.s.demo.db.MasterSlaveDataSourceAop     : 标记为从库
[           main] c.s.demo.db.MasterSlaveDataSource        : 数据库路由:从库 = slave3
[           main] c.s.demo.test.DemoApplicationTests       : read=slave3
[           main] c.s.demo.db.MasterSlaveDataSourceAop     : 标记为从库
[           main] c.s.demo.db.MasterSlaveDataSource        : 数据库路由:从库 = slave1
[           main] c.s.demo.test.DemoApplicationTests       : read=slave1
[           main] c.s.demo.db.MasterSlaveDataSourceAop     : 标记为主库
[           main] c.s.demo.db.MasterSlaveDataSource        : 数据库路由:主库
[           main] c.s.demo.test.DemoApplicationTests       : write=new name

你可以看到,对于只读业务。确实轮询了三个不同的从库,符合预期。最后的 write() 方法也成功地路由到了主库,执行了修改并且返回了修改后的结果。

十一总结

通过 AbstractRoutingDataSource 可以不使用任何第三方中间件就可以在 Spring Boot 中实现数据源 “读写分离”,这种方式需要在每个业务方法上通过 @Transactional 注解明确定义是读还是写。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/132217.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

http进一步认识

好久不见各位&#xff0c;今天为大家带来http协议的进一步认识 文章目录 &#x1f440;http协议的认识&#x1f440;新的改变 &#x1f440;http协议的认识 http协议经历了三个版本的演化&#xff0c;HTTP0.9是第一个版本的协议&#xff0c;它的组成极其简单&#xff0c;只涉…

硬科技企业社区“曲率引擎”品牌正式发布

“曲率引擎”&#xff0c;是科幻作品中最硬核的加速系统&#xff0c;通过改变时空的曲率&#xff0c;可实现光速飞行甚至能够超越光速。11月3日&#xff0c;“曲率引擎&#xff08;warp drive&#xff09;”作为硬科技企业社区品牌&#xff0c;在2023全球硬科技创新大会上正式对…

Oracle(12)Managing Indexes

目录 目标&#xff1a; 一、基础知识 1、Classification ofindexes 索引的分类 2、B-Tree vs Bitmap 3、Creating Indexes: Guidelines 创建索引:准则 4、Offline Index Rebuild 脱机索引重建 5、RebuildingIndexes 重建索引 6、Online Index Rebuild 在线索引重建 7…

第22期 | GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练 Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大型语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以…

前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案

目录 场景还原相关代码开发者工具 - 网络请求记录 问题排查定位改bug 总结 场景还原 我在前端使用axios接收后端xlsx表格文件流并下载&#xff0c;xlsx文件能够下载成功&#xff0c;但是打开却显示文件无法打开 相关代码 请求API封装:Content–Type以及responseType经核对均…

虚拟机Linux-Centos系统网络配置常用命令+Docker 的常用命令

目录 1、虚拟机Linux-Centos系统网络配置常用命令2、Docker 的常用命令2.1 安装docker步骤命令2.2 在docker容器中安装和运行mysql 2、dockerfile关键字区别(ADD/COPY,CMD/ENTRYPOINT) 1、虚拟机Linux-Centos系统网络配置常用命令 进入网络配置文件目录 cd /etc/sysconfig/ne…

【webrtc】 对视频质量的码率控制的测试与探索

目录 环境设置 transport-cc goog-remb (webrtc中的两种码率算法&#xff09; 修改成remb算法 测试 效果 后续 可参考工程 环境设置 要到meshx上操作 telnet 112 然后执行factory_env show |grep meshx_ip 之后telnet meshx_ip 用户名admin 密码****.119 执行一下r…

Leetcode-88 合并两个有序数组

使用内置排序函数&#xff0c;时间复杂度On^2 class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int j0,im;while(j<n){nums1[i]nums2[j];}Arrays.sort(nums1);} }新建一个临时数组用于放排序后的元素&#xff0c;再将临时数组赋值给nums1&…

IT运营与DevOps:有何不同?

IT 运营和 DevOps 满足许多现代企业密切相关的需求。然而&#xff0c;尽管这两种角色之间有许多相似之处&#xff0c;但也有重要的区别&#xff0c;将 IT 运营与 DevOps 混为一谈是错误的。 本文通过解释每种类型的角色是做什么的&#xff1b;它们在流程、工具和文化方面的比…

【软件STM32cubeIDE下H73xx配置串口uart1+中断接收/DMA收发+HAL库+简单数据解析-基础样例】

#【软件STM32cubeIDE下H73xx配置串口uart1中断接收/DMA收发HAL库简单数据解析-基础样例】 1、前言2、实验器件3-1、普通收发中断接收实验第一步&#xff1a;代码调试-基本配置&#xff08;1&#xff09;基本配置&#xff08;3&#xff09;时钟配置&#xff08;4&#xff09;保存…

大神接力 | YOLOv4算法超详细解析(包括诞生背景+论文解析+技术原理等)

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。YOLOv4论文的发表背景是在原作者声名放弃更新YOLO算法后&#xff0c;俄罗斯的Alexey大神扛起了YOLOv4的大旗&#xff0c;因此&#xff0c;其诞生背景是为了进一步提高目标检测算法的性能和精度。本篇文章就简单讲述一下YOL…

每个程序员都应该自己写一个的:socket包装类

每个程序员都应该有自己的网络类。 下面是我自己用的socket类&#xff0c;支持所有我自己常用的功能&#xff0c;支持windows和unix/linux。 目录 客户端 服务端 非阻塞 获取socket信息 完整代码 客户端 作为socket客户端&#xff0c;只需要如下几个功能&#xff1a; //…

Java 8 新特性 Stream 的使用场景(不定期更新)

方便在写代码的过程中直接使用&#xff0c;好记性不如好文章&#xff0c;直接 CV 改了直接用。提高 办&#xff08;摸&#xff09;公&#xff08;鱼&#xff09;效&#xff08;时&#xff09;率&#xff08;间&#xff09;&#xff0c; 不然就直接问 GPT 也不是说不行。 只符合…

Mysql学习文档笔记

文章目录 基础篇通用语法及分类DDL&#xff08;数据定义语言&#xff09;数据库操作注意事项 表操作 DML&#xff08;数据操作语言&#xff09;添加数据注意事项 更新和删除数据 DQL&#xff08;数据查询语言&#xff09;基础查询条件查询聚合查询&#xff08;聚合函数&#xf…

【C语法学习】18 - fread()函数

文章目录 1 函数原型2 参数3 返回值4 示例 1 函数原型 fread()&#xff1a;从与指定流stream相关联的二进制文件中读取数据块储存在str指向的内存空间中&#xff0c;函数原型如下&#xff1a; size_t fread(const void *ptr, size_t size, size_t count, FILE *stream)2 参数…

[动态规划] (七) 路径问题:LCR 166.剑指offer 47. 珠宝的最高价值

[动态规划] (七) 路径问题&#xff1a;LCR 166./剑指offer 47. 珠宝的最高价值 文章目录 [动态规划] (七) 路径问题&#xff1a;LCR 166./剑指offer 47. 珠宝的最高价值题目解析解题思路状态表示状态转移方程初始化和填表顺序 返回值代码实现总结 LCR 166. 珠宝的最高价值 题目…

python web框架 flask基础入门教程

python web框架 flask基础入门教程 今天我们写一个flask基础入门教程&#xff0c;当然也会覆盖很多重要的知识点&#xff0c;在这篇博客中&#xff0c;我们主要会讲解如下内容&#xff1a; 1、通过flask框架向web传输和接收参数 2、实现静态图片插入和图书上传 3、实现搭建…

宝塔面板使用Supervisor进程守护插件,配置守护Mysql的操作教程。

本篇文章主要讲解&#xff0c;在宝塔面板中使用Supervisor进程守护插件&#xff0c;配置守护Mysql的操作教程。 作者&#xff1a;任聪聪 日期&#xff1a;2023年11月5日 一、安装守护进程插件 安装插件一、进程守护插件 安装说明&#xff1a;在软件商店中搜索“进程守护”&am…

VC++常用命名法和宏定义

匈牙利命名法规则 一般情况下&#xff0c;变量的取名方式为&#xff1a; <scope_> <prefix_> <qualifier>。 范围前缀_&#xff0c;类型前缀_&#xff0c;限定词。 特殊的类型命名,前缀表示&#xff1a; 类、接口 前缀 类型 例子 备注 Lm Class …

【Vue】使用v-model实现控制子组件显隐

v-model 可以实现双向绑定的效果&#xff0c;允许父组件控制子组件的显示/隐藏&#xff0c;同时允许子组件自己控制自身的显示/隐藏。以下是如何使用 v-model 实现这个需求&#xff1a; 在父组件中&#xff0c;你可以使用 v-model 来双向绑定一个变量&#xff0c;这个变量用于…