SpringBoot 多数据源及事务解决方案
1. 背景
一个主库和N个应用库的数据源,并且会同时操作主库和应用库的数据,需要解决以下两个问题:
-
如何动态管理多个数据源以及切换?
-
如何保证多数据源场景下的数据一致性(事务)?
本文主要探讨这两个问题的解决方案,希望能对读者有一定的启发。
2. 数据源切换原理
通过扩展Spring提供的抽象类AbstractRoutingDataSource,可以实现切换数据源。其类结构如下图所示:
-
targetDataSources&defaultTargetDataSource
项目上需要使用的所有数据源和默认数据源。
-
resolvedDataSources&resolvedDefaultDataSource
当Spring容器创建AbstractRoutingDataSource对象时,通过调用afterPropertiesSet复制上述目标数据源。由此可见,一旦数据源实例对象创建完毕,业务无法再添加新的数据源。
-
determineCurrentLookupKey
此方法为抽象方法,通过扩展这个方法来实现数据源的切换。目标数据源的结构为:Map<Object, DataSource>其key为lookup key。
我们来看官方对这个方法的注释:
lookup key通常是绑定在线程上下文中,根据这个key去resolvedDataSources中取出DataSource。
根据目标数据源的管理方式不同,可以使用基于配置文件和数据库表两种方式。基于配置文件管理方案无法后续添加新的数据源,而基于数据库表方案管理,则更加灵活。
3. 配置文件解决方案
根据上面的分析,我们可以按照下面的步骤去实现:
-
定义
DynamicDataSource类继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法。 -
配置多个数据源注入
targetDataSources和defaultTargetDataSource,通过afterPropertiesSet()方法将数据源写入resolvedDataSources和resolvedDefaultDataSource。 -
调用
AbstractRoutingDataSource的getConnection()方法时,determineTargetDataSource()方法返回DataSource执行底层的getConnection()。
其流程如下图所示:
3.1 创建数据源
DynamicDataSource数据源的注入,目前业界主流实现步骤如下:
在配置文件中定义数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
# 主数据源
spring.datasource.druid.master.url=jdbcUrl
spring.datasource.druid.master.username=***
spring.datasource.druid.master.password=***
# 其他数据源
spring.datasource.druid.second.url=jdbcUrl
spring.datasource.druid.second.username=***
spring.datasource.druid.second.password=***
在代码中配置Bean
@Configuration
public class DynamicDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.druid.master")public DataSource firstDataSource(){return DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.druid.second")public DataSource secondDataSource(){return DruidDataSourceBuilder.create().build();}@Bean@Primarypublic DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {Map<Object, Object> targetDataSources = new HashMap<>(5);targetDataSources.put(DataSourceNames.FIRST, firstDataSource);targetDataSources.put(DataSourceNames.SECOND, secondDataSource);return new DynamicDataSource(firstDataSource, targetDataSources);}
}
3.2 AOP处理
通过DataSourceAspect切面技术来简化业务上的使用,只需要在业务方法添加@SwitchDataSource注解即可完成动态切换:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SwitchDataSource {String value();
}
DataSourceAspect拦截业务方法,更新当前线程上下文DataSourceContextHolder中存储的key,即可实现数据源切换。
3.3 方案不足
基于AbstractRoutingDataSource的多数据源动态切换,有个明显的缺点,无法动态添加和删除数据源。在我们的产品中,不能把应用数据源写死在配置文件。接下来分享一下基于数据库表的实现方案。
4. 数据库表解决方案
我们需要实现可视化的数据源管理,并实时查看数据源的运行状态。所以我们不能把数据源全部配置在文件中,应该将数据源定义保存到数据库表。参考AbstractRoutingDataSource的设计思路,实现自定义数据源管理。
4.1 设计数据源表
主库的数据源信息仍然配置在项目配置文件中,应用库数据源配置参数,则设计对应的数据表。表结构如下所示:
这个表主要就是DataSource的相关配置参数,其相应的ORM操作代码在此不再赘述,主要是实现数据源的增删改查操作。
4.2 自定义数据源管理
4.2.1 定义管理接口
通过继承AbstractDataSource即可实现DynamicDataSource。为了方便对数据源进行操作,我们定义一个接口DataSourceManager,为业务提供操作数据源的统一接口。
public interface DataSourceManager {void put(String var1, DataSource var2);DataSource get(String var1);Boolean hasDataSource(String var1);void remove(String var1);void closeDataSource(String var1);Collection<DataSource> all();
}
该接口主要是对数据表中定义的数据源,提供基础管理功能。
4.2.2 自定义数据源
DynamicDataSource的实现如下图所示:
根据前面的分析,AbstractRoutingDataSource是在容器启动的时候,执行afterPropertiesSet注入数据源对象,完成之后无法对数据源进行修改。DynamicDataSource则实现DataSourceManager接口,可以将数据表中的数据源加载到dataSources。
4.2.3 切面处理
这一块的处理跟配置文件数据源方案处理方式相同,都是通过AOP技术切换lookup key。
public DataSource determineTargetDataSource() {String lookupKey = DataSourceContextHolder.getKey();DataSource dataSource = Optional.ofNullable(lookupKey).map(dataSources::get).orElse(defaultDataSource);if (dataSource == null) {throw new IllegalStateException("Cannot determine DataSource for lookup key [" + lookupKey + "]");}return dataSource;}
4.2.4 管理数据源状态
在项目启动的时候,加载数据表中的所有数据源,并执行初始化。初始化操作主要是使用SpringBoot提供的DataSourceBuilder类,根据数据源表的定义创建DataSource。在项目运行过程中,可以使用定时任务对数据源进行保活,为了提升性能再添加一层缓存。
AbstractRoutingDataSource 只支持单库事务,切换数据源是在开启事务之前执行。 Spring使用 DataSourceTransactionManager进行事务管理。开启事务,会将数据源缓存到DataSourceTransactionObject对象中,后续的commit和 rollback事务操作实际上是使用的同一个数据源。
如何解决切库事务问题?借助Spring的声明式事务处理,我们可以在多次切库操作时强制开启新的事务:
@SwitchDataSource
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
这样的话,执行切库操作的时候强制启动新事务,便可实现多次切库而且事务能够生效。但是这种事务方式,存在数据一致性问题:

假若ServiceB正常执行提交事务,接着返回ServiceA执行并且发生异常。因为两次处理是不同的事务,ServiceA这个事务执行回滚,而ServiceA事务已经提交。这样的话,数据就不一致了。接下来,我们主要讨论如何解决多库的事务问题。
6. 多库事务处理
6.1 关于事务的理解
首先有必要理解事务的本质。
1.提到Spring事务,就离不开事务的四大特性和隔离级别、七大传播特性。
事务特性和离级别是属于数据库范畴。Spring事务的七大传播特性是什么呢?它是Spring在当前线程内,处理多个事务操作时的事务应用策略,数据库事务本身并不存在传播特性。

2.Spring事务的定义包括:begin、commit、rollback、close、suspend、resume等动作。
-
begin(事务开始): 可以认为存在于数据库的命令中,比如Mysql的
start transaction命令,但是在JDBC编程方式中不存在。 -
close(事务关闭): Spring事务的close()方法,是把
Connection对象归还给数据库连接池,与事务无关。 -
suspend(事务挂起): Spring中事务挂起的语义是:需要新事务时,将现有的
Connection保存起来(还有尚未提交的事务),然后创建新的Connection2,Connection2提交、回滚、关闭完毕后,再把Connection1取出来继续执行。 -
resume(事务恢复): 嵌套事务执行完毕,返回上层事务重新绑定连接对象到事务管理器的过程。
实际上,只有commit、rollback、close是在JDBC真实存在的,而其他动作都是应用的语意,而非JDBC事务的真实命令。因此,事务真实存在的方法是:setAutoCommit()、commit()、rollback()。
close()语义为:
-
关闭一个数据库连接,这已经不再是事务的方法了。
使用DataSource并不会执行物理关闭,只是归还给连接池。
6.2 自定义管理事务
为了保证在多个数据源中事务的一致性,我们可以手动管理Connetion的事务提交和回滚。考虑到不同ORM框架的事务管理实现差异,要求实现自定义事务管理不影响框架层的事务。
这可以通过使用装饰器设计模式,对Connection进行包装重写commit和rolllback屏蔽其默认行为,这样就不会影响到原生Connection和ORM框架的默认事务行为。其整体思路如下图所示:
这里并没有使用前面提到的@SwitchDataSource,这是因为我们在TransactionAop中已经执行了lookupKey的切换。
6.2.1 定义多事务注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransaction {String transactionManager() default "multiTransactionManager";// 默认数据隔离级别,随数据库本身默认值IsolationLevel isolationLevel() default IsolationLevel.DEFAULT;// 默认为主库数据源String datasourceId() default "default";// 只读事务,若有更新操作会抛出异常boolean readOnly() default false;
业务方法只需使用该注解即可开启事务,datasourceId指定事务用到的数据源,不指定默认为主库。
6.2.3 包装Connection
自定义事务我们使用包装过的Connection,屏蔽其中的commit&rollback方法。这样我们就可以在主事务里进行统一的事务提交和回滚操作。
public class ConnectionProxy implements Connection {private final Connection connection;public ConnectionProxy(Connection connection) {this.connection = connection;}@Overridepublic void commit() throws SQLException {// connection.commit();}public void realCommit() throws SQLException {connection.commit();}@Overridepublic void close() throws SQLException {//connection.close();}public void realClose() throws SQLException {if (!connection.getAutoCommit()) {connection.setAutoCommit(true);}connection.close();}@Overridepublic void rollback() throws SQLException {if(!connection.isClosed())connection.rollback();}...
}
这里commit&close方法不执行操作,rollback执行的前提是连接执行close才生效。这样不管是使用哪个ORM框架,其自身事务管理都将失效。事务的控制就交由MultiTransaction控制了。
6.2.4 事务上下文管理
public class TransactionHolder {// 是否开启了一个MultiTransactionprivate boolean isOpen;// 是否只读事务private boolean readOnly;// 事务隔离级别private IsolationLevel isolationLevel;// 维护当前线程事务ID和连接关系private ConcurrentHashMap<String, ConnectionProxy> connectionMap;// 事务执行栈private Stack<String> executeStack;// 数据源切换栈private Stack<String> datasourceKeyStack;// 主事务IDprivate String mainTransactionId;// 执行次数private AtomicInteger transCount;// 事务和数据源key关系private ConcurrentHashMap<String, String> executeIdDatasourceKeyMap;}
每开启一个事物,生成一个事务ID并绑定一个ConnectionProxy。事务嵌套调用,保存事务ID和lookupKey至栈中,当内层事务执行完毕执行pop。这样的话,外层事务只需在栈中执行peek即可获取事务ID和lookupKey。
6.2.5 数据源兼容处理
为了不影响原生事务的使用,需要重写getConnection方法。当前线程没有启动自定义事务,则直接从数据源中返回连接。
@Overridepublic Connection getConnection() throws SQLException {TransactionHolder transactionHolder = MultiTransactionManager.TRANSACTION_HOLDER_THREAD_LOCAL.get();if (Objects.isNull(transactionHolder)) {return determineTargetDataSource().getConnection();}ConnectionProxy ConnectionProxy = transactionHolder.getConnectionMap().get(transactionHolder.getExecuteStack().peek());if (ConnectionProxy == null) {// 没开跨库事务,直接返回return determineTargetDataSource().getConnection();} else {transactionHolder.addCount();// 开了跨库事务,从当前线程中拿包装过的Connectionreturn ConnectionProxy;}}
6.2.6 切面处理
切面处理的核心逻辑是:维护一个嵌套事务栈,当业务方法执行结束,或者发生异常时,判断当前栈顶事务ID是否为主事务ID。如果是的话这时候已经到了最外层事务,这时才执行提交和回滚。详细流程如下图所示:
package com.github.mtxn.transaction.aop;import com.github.mtxn.application.Application;
import com.github.mtxn.transaction.MultiTransactionManager;
import com.github.mtxn.transaction.annotation.MultiTransaction;
import com.github.mtxn.transaction.context.DataSourceContextHolder;
import com.github.mtxn.transaction.support.IsolationLevel;
import com.github.mtxn.transaction.support.TransactionHolder;
import com.github.mtxn.utils.ExceptionUtils;
import lombok.extern.slf4j.Slf4j;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Aspect
@Component
@Slf4j
@Order(99999)
public class MultiTransactionAop {@Pointcut("@annotation(com.github.mtxn.transaction.annotation.MultiTransaction)")public void pointcut() {if (log.isDebugEnabled()) {log.debug("start in transaction pointcut...");}}@Around("pointcut()")public Object aroundTransaction(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();// 从切面中获取当前方法Method method = signature.getMethod();MultiTransaction multiTransaction = method.getAnnotation(MultiTransaction.class);if (multiTransaction == null) {return point.proceed();}IsolationLevel isolationLevel = multiTransaction.isolationLevel();boolean readOnly = multiTransaction.readOnly();String prevKey = DataSourceContextHolder.getKey();MultiTransactionManager multiTransactionManager = Application.resolve(multiTransaction.transactionManager());// 切数据源,如果失败使用默认库if (multiTransactionManager.switchDataSource(point, signature, multiTransaction)) return point.proceed();// 开启事务栈TransactionHolder transactionHolder = multiTransactionManager.startTransaction(prevKey, isolationLevel, readOnly, multiTransactionManager);Object proceed;try {proceed = point.proceed();multiTransactionManager.commit();} catch (Throwable ex) {log.error("execute method:{}#{},err:", method.getDeclaringClass(), method.getName(), ex);multiTransactionManager.rollback();throw ExceptionUtils.api(ex, "系统异常:%s", ex.getMessage());} finally {// 当前事务结束出栈String transId = multiTransactionManager.getTrans().getExecuteStack().pop();transactionHolder.getDatasourceKeyStack().pop();// 恢复上一层事务DataSourceContextHolder.setKey(transactionHolder.getDatasourceKeyStack().peek());// 最后回到主事务,关闭此次事务multiTransactionManager.close(transId);}return proceed;}}
7.总结
本文主要介绍了多数据源管理的解决方案(应用层事务,而非XA二段提交保证),以及对多个库同时操作的事务管理。
需要注意的是,这种方式只适用于单体架构的应用。因为多个库的事务参与者都是运行在同一个JVM进行。如果是在微服务架构的应用中,则需要使用分布式事务管理(譬如:Seata)。
相关文章:
SpringBoot 多数据源及事务解决方案
1. 背景 一个主库和N个应用库的数据源,并且会同时操作主库和应用库的数据,需要解决以下两个问题: 如何动态管理多个数据源以及切换? 如何保证多数据源场景下的数据一致性(事务)? 本文主要探讨这两个问题的解决方案…...
tcpdump使用教程
一、概述 tcpdump是一个功能强大的,用于抓取网络数据包的命令行工具,与带界面的Wireshark一样,基于libpcap库构建。这篇文章主要介绍tcpdump的使用。关于如何使用tcpdump的资料中,最有用的就是tcpdump的两个手册。 tcpdump使用手…...
Zynq-7000、FMQL45T900的GPIO控制(五)---linux应用层配置GPIO输出控制
上文中详细阐述了对应原理图MIO/EMIO的编号,怎么计算获取linux下gpio的编号 本文涉及C代码上传,下载地址 Zynq-7000、FMQL45T900的GPIO控制c语言代码资源-CSDN文库 本文详细记录一下针对获取到gpio的编号,进行配置输出模式,并进…...
带你搞懂人工智能、机器学习和深度学习!
不少高校的小伙伴找我聊入门人工智能该怎么起步,如何快速入门,多长时间能成长为中高级工程师(聊下来感觉大多数学生党就是焦虑,毕业即失业,尤其现在就业环境这么差),但聊到最后,很多…...
Android 11.0 framework中Launcher的启动流程分析
1.前言 在11.0的系统rom定制化开发中,在rom定制过程中,在对于开发默认Launcher功能,解决开机动画后黑屏,了解fallbackhome机制等等 对于launcher的启动流程来说很重要,接下来就来分析下launcher的启动流程 2.framework中Launcher的启动流程分析的核心类 frameworks/ba…...
2023年第十五届华中杯赛题C 题 空气质量预测与预警
2023年五一假期期间,数学建模竞赛就有四场,各种比赛各种需求应接不暇。因此,对于本次浅析有不足的地方欢迎大家指出。为了更好的帮助大家华中杯参赛,下面带来,C题详细版思路。由于C题的难度,注定选题人数将…...
Go官方指南(一)包、变量、函数
import "time" 获取当前系统时间:time.Now() 每个 Go 程序都是由包构成的 按照约定 ,包名与导入路径的最后一个元素一致。例如,"math/rand"包中的源码均以 package rand 语句开始 在 Go 中,如果一个名字以…...
liunx笔记
快捷键 #移动到行首 ctrla #移动到行尾 ctrle #删除光标之前的字符 ctrlu #删除光标之后的字符 ctrlk #清屏 ctrll正则表达式 正则中普通常用的元字符 元字符功能.匹配除了换行符以外的任意单个字符*前导字符出现0次或连续多次.*任意长度字符^行首(以…开头),如…...
vue3 封装ECharts组件
一、前言 前端开发需要经常使用ECharts图表渲染数据信息,在一个项目中我们经常需要使用多个图表,选择封装ECharts组件复用的方式可以减少代码量,增加开发效率。 ECharts图表大家应该用的都比较多,基础的用法就不细说了ÿ…...
Spring Security 6.0系列【30】授权服务器篇之JOSE规范
有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot 版本 3.0.4 本系列Spring Security 版本 6.0.2 本系列Spring Authorization Server 版本 1.0.2 源码地址:https://gitee.com/pearl-organization/study-spring-security-demo 文章目录 1. 前言2. JOSE 规范3. JW…...
维度表设计原则
维度的作用一般是查询约束、分类汇总以及排序等,我们在进行维度表设计时,应当提前考虑: (1)维度属性尽量丰富,为数据使用打下基础 比如淘宝商品维度有近百个维度属性,为下游的数据统计、分析、…...
【requests模块上】——02爬虫基础——如桃花来
目录索引 requests请求:1. 基于get请求:*基础写法:**带参数的get请求:* 2. 基于post请求: 获取数据:1. 获取json数据:2. 获取二进制数据: 初步伪装小爬虫——添加headers: 引入&…...
Springboot +Flowable,详细解释啥叫流程实例(一)
一.简介 上一篇中学习了Flowable 中的流程模板(流程定义)的部署问题,这一篇来学习什么叫流程实例。 部署之后的流程模板,还不能直接运行,例如我们部署了一个请假流程,现在 张三想要请假,他就需…...
信息安全复习十:Web与电子商务安全
一、章节梗概 1.信息安全的学科内容 2.Web和电子商务安全问题提出 3.安全套接字协议SSL与传输层安全协议TLS 4.安全电子交易(SET)简要介绍 复习: 密码学内容:对称密钥密码、公开密钥密码、报文鉴别 PKI:数字签名、数字证书、信任关系 身份认…...
flutter 启动其他app server或者页面失败
1.目标Service 设置 android:exported"true" 2.目标Service需要声明自定义权限。客户端需要声明权限。 3.目标Service需要添加<intent-filter></intent-filter> 检查以上的声明和权限, 如果还是不行 说明是 Android 11引入了*包可见性*’ …...
【linux-进程2】进程控制
🌈环境变量 🍄初识 系统带的命令可以直接运行(ls ll命令等),但是我们自己写的命令必须要带上路径才能运行(./myproc),这是什么原因导致的?如果我们也想自己写的命令直接…...
【五一创作】多域名环境和Office 365混合部署方案
目录 一、多域名环境是什么? 二、Office 365是什么? 三、多域名环境与Office 365的结合 总结 一、多域名环境是什么? 多域名环境指的是一个企业拥有多个域名,这些域名可能隶属于不同的子公司、部门或者品牌,但是都归属于同一个母公司。例如,一个中国电信集团旗下有…...
Vue:路由route
一、概念 1、组成 每一个路由都由 key 和 value 组成。 keyvalue路由 route。 2、本质 路由的本质:一个路由表达了一组对应关系。路由器的本质:管理多组对应关系。 3、路由的工作原理 点击之后路径变化——>路由器监视到变化——>根据路径…...
Windows系统被faust勒索病毒攻击勒索病毒解密服务器与数据库解密恢复
在近期,一种名为faust后缀的勒索病毒威胁已经引起了全球计算机系统安全领域的关注。faust勒索病毒是一种基于RSA加密算法的恶意软件,能够加密目标计算机系统上的所有文件,并向用户勒索赎金来承诺解密恢复操作。下面为大家介绍一下Windows系统…...
Java面试题总结 | Java面试题总结7- Redis模块(持续更新)
Redis 文章目录 Redisredis的线程模型Redis的Mysql的区别Redis和传统的关系型数据库有什么不同?Redis常见的数据结构zset数据结构Redis中rehash过程redis为什么不考虑线程安全的问题呢Redis单线程为什么还能这么快?为什么Redis是单线程的?red…...
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...
Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
虚拟电厂发展三大趋势:市场化、技术主导、车网互联
市场化:从政策驱动到多元盈利 政策全面赋能 2025年4月,国家发改委、能源局发布《关于加快推进虚拟电厂发展的指导意见》,首次明确虚拟电厂为“独立市场主体”,提出硬性目标:2027年全国调节能力≥2000万千瓦࿰…...
2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...
Unity VR/MR开发-VR开发与传统3D开发的差异
视频讲解链接:【XR马斯维】VR/MR开发与传统3D开发的差异【UnityVR/MR开发教程--入门】_哔哩哔哩_bilibili...
算术操作符与类型转换:从基础到精通
目录 前言:从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符:、-、*、/、% 赋值操作符:和复合赋值 单⽬操作符:、--、、- 前言:从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...
6.9-QT模拟计算器
源码: 头文件: widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QMouseEvent>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);…...
用递归算法解锁「子集」问题 —— LeetCode 78题解析
文章目录 一、题目介绍二、递归思路详解:从决策树开始理解三、解法一:二叉决策树 DFS四、解法二:组合式回溯写法(推荐)五、解法对比 递归算法是编程中一种非常强大且常见的思想,它能够优雅地解决很多复杂的…...
