Spring系列-10 事务机制

news2024/10/7 6:37:39

背景:

在 事务-1 事务隔离级别和Spring事务传播机制 中对事务的特性、隔离级别、Spring事务的传播机制结合案例进行了分析;在 事务-2 Spring与Mybatis事务实现原理
中对JDBC、Mybatis、Spring整合Mybatis实现事务的原理结合框架源码进行了介绍,过程中对SqlSession和SqlSessionTemplate的线程安全性也进行了说明。
本文以前两篇文章为基础,补充了一些遗漏的知识点(偏向于Spring),建议读者先阅读完上述两篇文章,有任何疑问或问题可以在品论区留言。

1.Spring事务

ORM框架是连接代码和数据库的桥梁,Spring作为基础框架提供了Spring Data JPA,也提供了适配其他ORM框架的能力,如集成Mybatis和Hibernate等。需要注意ORM框架提供的事务能力依赖于数据库事务,是对数据库事务的一层封装;如果底层数据库不支持事务(如Mysql的MyISAM引擎),在此之上的所有数据库或者DAO操作都无事务特性。
Spring事务提供了两种使用方式:声明式 (通过@Transactional注解的方式) 和编程式 (使用TransactionTemplate包裹代码方式)。其中,声明式使用较为简单且不容易出错;编程式相对比较灵活,可以进行细粒度控制。

声明式和编程式的底层实现原理相同,因编程式不常见,下文以声明式为例对Spring事务进行介绍。

对于声明式事务,Spring通过AOP机制将业务代码包裹起来,在业务代码前后开启和关闭事务,根据执行情况以及自定义条件进行事务的提交或回滚;提交/回滚时需要先获取对应的Connection对象,然后基于该对象进行。如何保证执行sql语句时的Connection对象和提交/回滚时的Connection对象为同一对象,以及同一线程在不同事务上下文的执行过程中获取的Connection对象为不是同一个;前者说明了Spring事务的底层原理,后者提供了线程安全保证。
这部分内容是本文的重点内容,也是理解Spring事务概念的关键;其实读者心里已经有概念了:ThreadLocal。

2.使用方式

参考: 事务-1 事务隔离级别和Spring事务传播机制 和 事务-2 Spring与Mybatis事务实现原理

3.实现原理

Spring Boot项目中常见的ORM框架有JPA、Hibernate、Mybatis, 本章节以Spring Boot整合Mybatis的业务场景为例对Spring事务进行说明。

3.1 Spring事务组件

在Spring项目中,可以通过xml配置(配置AOP)或注解(@EnableTransactionManagement)的方式开启事务功能;但都需要定义PlatformTransactionManager类型的Bean对象, 如下所示:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>

而SpringBoot项目通过自动装配机制,默认开启了Spring事务。

Spring为支持事务功能提供了InfrastructureAdvisorAutoProxyCreator后置处理器、TransactionInterceptor拦截器和BeanFactoryTransactionAttributeSourceAdvisor增强等组件,在本章节后续中会陆续进行介绍。

3.2 代理对象

InfrastructureAdvisorAutoProxyCreator同 Spring系列-8 AOP使用与原理 文中介绍的AnnotationAwareAspectJAutoProxyCreator均为AbstractAdvisorAutoProxyCreator的实现类,而AOP逻辑被封装在了AbstractAdvisorAutoProxyCreator中,且在Spring系列-8 AOP使用与原理 中已对该部分进行了较为全面地介绍,因此相同部分认为读者已知,本文不再赘述。
InfrastructureAdvisorAutoProxyCreator作为BPP,在对象初始化后期会调用InfrastructureAdvisorAutoProxyCreator的postProcessAfterInitialization方法:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
	if (bean != null) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
			return wrapIfNecessary(bean, beanName, cacheKey);
		}
	}
	return bean;
}

不考虑循环依赖场景,进入wrapIfNecessary(bean, beanName, cacheKey)方法:

// 省略缓存、日志、异常分支
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
	Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
	if (specificInterceptors != DO_NOT_PROXY) {
		return createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
	}
	return bean;
}

逻辑较为清晰:先通过getAdvicesAndAdvisorsForBean获取增强逻辑,后然通过动态代理技术将增强逻辑织入到目标对象中。
跟进getAdvicesAndAdvisorsForBean方法,查看获取增强逻辑部分的源码实现:

// AbstractAdvisorAutoProxyCreator类中
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
	List<Advisor> candidateAdvisors = findCandidateAdvisors();
	List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
	extendAdvisors(eligibleAdvisors);
	if (!eligibleAdvisors.isEmpty()) {
		eligibleAdvisors = sortAdvisors(eligibleAdvisors);
	}
	return eligibleAdvisors;
}

从IOC中获取所有Avisor类型的Bean对象,过滤出符合条件的部分,排序后返回;整体逻辑同Spring系列-8 AOP使用与原理,区别在于过滤部分:findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
过滤逻辑如下:

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
	if (advisor instanceof IntroductionAdvisor) {
		return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
	} else if (advisor instanceof PointcutAdvisor) {
		PointcutAdvisor pca = (PointcutAdvisor) advisor;
		return canApply(pca.getPointcut(), targetClass, hasIntroductions);
	} else {
		// It doesn't have a pointcut so we assume it applies.
		return true;
	}
}

入参:
(1) Advisor advisor:为BeanFactoryTransactionAttributeSourceAdvisor类型,来自IOC容器;
(2) Class<?> targetClass:目标类的class对象;
(3) boolean hasIntroductions:事务增强不存在引介,为false;

BeanFactoryTransactionAttributeSourceAdvisor为PointcutAdvisor子类,进一步跟踪canApply方法:

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
	MethodMatcher methodMatcher = pc.getMethodMatcher();
	Set<Class<?>> classes = new LinkedHashSet<>();
	classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
	for (Class<?> clazz : classes) {
		Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
		for (Method method : methods) {
			if (methodMatcher.matches(method, targetClass)) {
				return true;
			}
		}
	}
	return false;
}

其中,methodMatcher.matches(method, targetClass)会返回方法是否添加了@Transactional注解,通过computeTransactionAttribute方法解析得到的TransactionAttribute是否为空进行确定:

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
	// Don't allow no-public methods as required.
	if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
		return null;
	}

	// The method may be on an interface, but we need attributes from the target class.
	// If the target class is null, the method will be unchanged.
	Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

	// First try is the method in the target class.
	TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
	if (txAttr != null) {
		return txAttr;
	}

	// Second try is the transaction attribute on the target class.
	txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
	if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
		return txAttr;
	}

	//。。。	省略
	return null;
}

首先判断方法是否为public, 非public方法返回false;尝试从目标方法中解析出TransactionAttribute对象,如果不为空则返回;否则再次尝试从目标类对象中解析TransactionAttribute对象。
因此,需要满足以下条件,才可以生产Spring事务代理对象,实现事务功能:
(1)目标对象是被IOC管理的Bean对象;
(2)需要在类或者方法上注解@Transactional;方法中的优先级高于类;类上的注解对整个类中的方法生效;
(3)要求方法必须为public和非static;

本节最后再看一下BeanFactoryTransactionAttributeSourceAdvisor增强对象的拦截器:
在这里插入图片描述
当完成代理后,TransationInterceptor拦截器作为增强逻辑被织入到了目标对象中;当目标方法被调用时,进行拦截器逻辑。

3.3 拦截器

当业务代码被调用时,进入动态代理的拦截器即TransactionInterceptor的invoke方法中:

public Object invoke(MethodInvocation invocation) throws Throwable {
	Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

	return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

MethodInvocation对象作为CGLIB代理拦截器方法的入参,持有目标对象的类型信息、目标方法以及调用目标方法的逻辑,分别对应上述:AopUtils.getTargetClass(invocation.getThis())invocation.getMethod()invocation::proceed.

invokeWithinTransaction的主线逻辑如下:

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation) {
		// ⚠️1.开启事务
		TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

		Object retVal;
		try {
		// ⚠️2.直接调用目标方法,得到返回结果
			retVal = invocation.proceedWithInvocation();
		} catch (Throwable ex) {
		// ⚠️3.异常场景处理-根据条件判断是否rollback
			completeTransactionAfterThrowing(txInfo, ex);
			throw ex;
		} finally {
			cleanupTransactionInfo(txInfo);
		}
		// ⚠️4.正常场景处理-commit
		commitTransactionAfterReturning(txInfo);
		return retVal;
	}
}

step1: 开启事务

Spring通过TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);开启事务,并将信息存储在TransactionInfo对象中。
在进入代码之前,先了解一下TransactionInfo对象的组成结构:
在这里插入图片描述
上图可以形象地表示为:
TransactionInfo持有TransactionStatus对象;
             TransactionStatus持有DataSourceTransactionObject
                            DataSourceTransactionObject持有ConnectionHolder
                                                    ConnectionHolder持有connection对象;
即,TransactionInfo属性中保存着connection对象信息。

跟进createTransactionIfNecessary方法的主线逻辑:

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
	
	txAttr = new DelegatingTransactionAttribute(txAttr) {
		@Override public String getName() { return joinpointIdentification;}
	};
	
	TransactionStatus status = tm.getTransaction(txAttr);
	
	return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

先介绍一下如参:
(1) PlatformTransactionManager tm: 从IOC容器中根据TransactionManager获取的Bean对象,此时为DataSourceTransactionManager类型;
(2) TransactionAttribute txAttr: 对@Transactional注解信息进行的封装;
(3) String joinpointIdentification:字符串类型,目标方法对应的 “包名.类名.方法名”。

createTransactionIfNecessary方法逻辑线如下:先对TransactionAttribute进行了一层简单封装DelegatingTransactionAttribute,增强了一个getName()方法,返回joinpointIdentification信息;
然后通过PlatformTransactionManager和TransactionAttribute获取TransactionStatus对象(该部分是关键);
最后将PlatformTransactionManager、DelegatingTransactionAttribute、TransactionStatus整合成TransactionInfo并返回。
其中,第二步中涉及Connection对象的创建以及事务的开启,需要引起重视:

// 删除日志和异常分支,并尽可能进行简化以突出主线逻辑:
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
	// 构造DataSourceTransactionObject对象
	Object transaction = doGetTransaction();
	
	return startTransaction(definition, transaction, false, null);
}

上述方法逻辑较为简单:创建一个DataSourceTransactionObject对象,创建一个空的ConnectionHolder对象并复制给DataSourceTransactionObject对象的connectionHolder属性。然后调用startTransaction方法开启事务:

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
		boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {

	boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
	DefaultTransactionStatus status = newTransactionStatus(
			definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
	doBegin(transaction, definition);
	prepareSynchronization(status, definition);
	return status;
}

startTransaction代码逻辑较为简单:根据已有的事务信息对构建DefaultTransactionStatus对象,并通过doBeginprepareSynchronization对其进行处理。
本文关心的内容在doBegin方法中:

protected void doBegin(Object transaction, TransactionDefinition definition) {
     DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
     // ⚠️1.根据DataSource对象创建Connection对象
     Connection newCon = this.obtainDataSource().getConnection();
     // ⚠️2.将Connection对象通过ConnectionHolder包装后赋值给DataSourceTransactionObject对象
     txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
     txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
     // ⚠️3.对DataSourceTransactionObject对象进行状态设置
     Connection con = txObject.getConnectionHolder().getConnection();
     Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
     txObject.setPreviousIsolationLevel(previousIsolationLevel);
     txObject.setReadOnly(definition.isReadOnly());
     if (con.getAutoCommit()) {
         txObject.setMustRestoreAutoCommit(true);
         con.setAutoCommit(false);
     }
     this.prepareTransactionalConnection(con, definition);
     txObject.getConnectionHolder().setTransactionActive(true);
     int timeout = this.determineTimeout(definition);
     if (timeout != -1) {
         txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
     }

     if (txObject.isNewConnectionHolder()) {
          // ⚠️4.将Connection对象绑定到线程上下文中
         TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
     }
 }

上述代码逻辑可以分为以下几个步骤:
(1)获取DataSource对象(此时为HikariDataSource), 然后基于该对象创建Connection对象;因此不同事务中 (即使同一线程) 的Connection不同。
(2)将Connection对象通过ConnectionHolder包装后赋值给DataSourceTransactionObject对象,用于后续的事务会滚或提交。
(3)为DataSourceTransactionObject设置超时时间、是否自动提交、隔离级别等;
(4)将Connection对象绑定到线程上下文中,为操作数据库时提供connection对象。进入TransactionSynchronizationManager类中的bindResource方法,ConnectionHolder对象被保存到resources对象中,该对象的定义如下:

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

即resources对象为ThreadLocal,这要求事务内部只有一个线程运行。对于如下场景,事务会失去其作用:
在这里插入图片描述

目标方法中包括了数据库操作1-4,线程1在执行目标方法前后开启和关闭了事务;执行过程中,开启了两个子线程:线程2用于处理数据库操作1,线程3用于处理数据库操作2,数据库操作3和数据库操作4在线程1中进行。此时数据库操作3和4在线程1开启的事务中生效;而数据库操作1和2成为了独立的事务。

step2: 直接调用目标方法,得到返回结果

createTransactionIfNecessary方法的入参InvocationCallback invocation类型是一个函数式接口:

@FunctionalInterface
protected interface InvocationCallback {
	@Nullable
	Object proceedWithInvocation() throws Throwable;
}

传参为:invocation::proceed,当InvocationCallback对象的proceedWithInvocation()方法被调用时,执行invocation::proceed这个lambda表达式,即目标对象的方法被执行。

step3: 回滚事务

当通过反射调用目标方法抛出Throwable类型的异常时,进入completeTransactionAfterThrowing(txInfo, ex);分支:

// 这里删除了日志和异常处理带阿妹,突出主线逻辑:
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
	if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
	// 回滚事务
		txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
	} else {
	// 提交事务
		txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
	}
}

txInfo.transactionAttribute属性中包含着注解在@Transactional中信息,如下所示:
在这里插入图片描述
其中,txInfo.transactionAttribute.rollbackOn(ex)用于校验目标方法执行时抛出的异常是否在指定异常范围内:范围之内满足回滚条件,触发回滚;否则,触发提交操作。

举例说明:

@Transactional(rollbackFor = Exception.class)
public void updateMoney(int money) {
    accountRepository.updateMoney(money);
}

通过@Transactional注解指定的异常类型为Exception,当accountRepository.updateMoney(money);语句执行抛出Exception或者其子类时(如IOException),执行回滚操作。

本文主题是介绍Spring实现事务的原理,不涉及Spring事务传播机制原理的介绍
因此,提交或者回滚关注主逻辑,略去事务传播机制的逻辑分支

回滚事务时,追踪txInfo.transactionAttribute.rollbackOn(ex)

protected void doRollback(DefaultTransactionStatus status) {
      DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
      Connection con = txObject.getConnectionHolder().getConnection();
      try {
          con.rollback();
      } catch (SQLException var5) {
          throw new TransactionSystemException("Could not roll back JDBC transaction", var5);
      }
  }

提交事务时,追踪txInfo.transactionAttribute.commit(ex)

protected void doCommit(DefaultTransactionStatus status) {
    DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    try {
        con.commit();
    } catch (SQLException var5) {
        throw new TransactionSystemException("Could not commit JDBC transaction", var5);
    }
}

无论rollback还是commit都依赖于Connection对象,即依赖于数据库的事务能力。
其中,获取Connection对象来自DefaultTransactionStatus对象,即来源于step1: 开启事务中创建的Connection对象。

step4: 提交事务

当目标方法执行过程中无异常发生时,进入以下逻辑:

protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
	if (txInfo != null && txInfo.getTransactionStatus() != null) {
		txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
	}
}

txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());step3: 回滚事务中介绍一致,用于提交事务对象。

3.4 数据库操作

3.3 拦截器节中的step2步骤中调用invocation::proceed表达式反射调用目标方法,本节中结合Mybatis案例进行细节分析。
案例如下所示:

// Mapper类及其配置文件
@Mapper
public interface AccountMapper {
    void updateMoney(int money);
}

<mapper namespace="com.seong.dao.AccountMapper">
    <update id="updateMoney" parameterType="java.lang.Integer">
        update t_account
        set money = #{money}
        where name = 'a'
    </update>
</mapper>

当AccountMapper(动态代理对象)的updateMoney方法被调用时,由拦截器进入SqlSessionTemplate的update方法:

public int update(String statement, Object parameter) {
    return this.sqlSessionProxy.update(statement, parameter);
}

sqlSessionProxy也是一个代理对象,再次进入拦截器对象中(上述两次拦截在前文提及的事务文章中有介绍,有需要请参考) :

// 尽可能删除分支,突出核心逻辑:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	// ⚠️1.获取sqlSession对象
	SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
	// ⚠️2.数据库操作
	Object result = method.invoke(sqlSession, args);
	// ⚠️3.关闭 SqlSession
    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
    return result;
}

这部分的逻辑是获取sqlSession对象,并执行数据库操作;注意这里也有一个ThreadLocal对象,读者可自行分析其作用。这里关注的重点是SqlSessionUtils.getSqlSession获取sqlsession对象以及执行过程。
SqlSession对象的属性和行为如下图所示:
在这里插入图片描述
通过executor间接包含了transaction事务对象,即每个SqlSession对象内部持有一个事务对象。事务对象创建之初内部的Connection属性为空,直到需要执行数据库操作时才会根据事务对象生成。通过SqlSessionUtils.getSqlSession方法得到的SqlSession对象也是如此。
追踪该方法的调用链,进入openSessionFromDataSource方法:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
	Environment environment = configuration.getEnvironment();
	TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
	Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
	Executor executor = configuration.newExecutor(tx, execType);
	return new DefaultSqlSession(configuration, executor, autoCommit);
}

从Mybatis的configuration配置中获取环境对象,并从环境对象中获取事务工厂;注意:这里的configuration和环境对象、事务工厂是全局唯一的。当Spring集成Mybatis时,TransactionFactory会被设置为SpringManagedTransactionFactory类型。由SpringManagedTransactionFactory事务工厂创建的对象自然为SpringManagedTransaction类型:

public class SpringManagedTransactionFactory implements TransactionFactory {
  public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
   	  return new SpringManagedTransaction(dataSource);
  }
}

当执行sql语句前先通过transaction对象获取数据库连接对象:

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
        return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
        return connection;
    }
}

进入SpringManagedTransaction的getConnection方法:

public Connection getConnection() throws SQLException {
   if (this.connection == null) {
      openConnection();
   }
   return this.connection;
}

private void openConnection() throws SQLException {
  this.connection = DataSourceUtils.getConnection(this.dataSource);
  this.autoCommit = this.connection.getAutoCommit();
  this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
}

所以,SpringManagedTransaction是通过DataSourceUtils.getConnection(this.dataSource)获取Connection对象,追踪其调用链进入doGetConnection方法:

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
    if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
      // ... 
    } else {
        conHolder.requested();
        return conHolder.getConnection();
    }
}

通过TransactionSynchronizationManager.getResource(dataSource)获取的ConnectionHolder对象正是开启事务时放入的。
因此,保证了事务在执行sql以及会滚/提交过程中使用的是同一个Connection对象。

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

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

相关文章

如何免费使用GPT-4模型

一、引言 OpenAI 最近发布了ChatGPT最新的 GPT-4 模型&#xff0c;这是 OpenAI 迄今为止发布的最强大的语言模型系统。它不仅有视觉能力&#xff0c;而且是多模态的&#xff0c;可以解释文本和生成图像。此外&#xff0c;它在推理测试中表现良好&#xff0c;可以支持大约26种不…

Redis的ZipList和QuickList和SkipList和RedisObject

ZipList:压缩列表&#xff0c;为了节省内存而设计的一种数据结构 ZipList是一种特殊的双端链表&#xff0c;是由一系列的特殊编码的连续内存块组成&#xff0c;不需要通过指针来进行寻址来找到各个节点&#xff0c;可以在任意一端进行压入或者是弹出操作&#xff0c;并且该操作…

RocketMQ的学习历程(5)----broker内部设计

文章目录 概要整体架构流程技术名词解释CommitLog和ConsumeQueue页缓存和内存映射刷盘机制 小结 概要 在首个学习历程中&#xff0c;我们已经了解了&#xff0c;RokctMQ简单的工作流程。 如果想要更深的理解RokcetMQ消息处理的流程&#xff0c;broker内部流程的理解是必要的&…

【挑战全站最全】Linux系统的安装与配置教程——以CentOS为例

&#x1f680;作者&#xff1a;那个叫马尔的大夫&#x1f680; ⭐专栏&#xff1a;操作系统⭐ &#x1f33c;内容&#xff1a;主要分享一些关于Linux操作系统的知识 &#x1f967;不忘初心&#xff0c;砥砺前行~ 目录 一、用到的软件环境——虚拟机软件&#xff08;必需&#…

调用函数不仅仅只是传递正确的参数类型

这里有一个新手犯下的一个典型错误。 假设&#xff0c;我们想调用这个函数&#xff0c;GetBinaryType。 void sample() { if (GetBinaryType(TEXT(“explorer.exe”), ????)) { … } } 请问&#xff0c;这里的问号处应该传递什么类型的参数&#xff1f;你可能会说&#x…

python、pyqt5实现人脸检测、性别和年龄预测

摘要&#xff1a;这篇博文介绍基于opencv&#xff1a;DNN模块自带的残差网络的人脸、性别、年龄识别系统&#xff0c;系统程序由OpenCv, PyQt5的库实现。如图系统可通过摄像头获取实时画面并识别其中的人脸表情&#xff0c;也可以通过读取图片识别&#xff0c;本文提供完整的程…

设计模式入门:策略模式

现有一套模拟鸭子游戏&#xff0c;可以一边游泳&#xff0c;一边呱呱叫。 每种鸭子都会呱呱叫和游泳&#xff0c;只是外观不同。因此&#xff0c;quack和swim放在父类中&#xff0c;display放在子类中实现。 增加新的功能&#xff1a;鸭子飞翔。 1 我们可能想到直接在父类中增…

LeetCode——最小化字符串长度

目录 一、题目 二、题目解读 三、代码 1、set去重 2、用一个二进制数记录每个字母是否出现过 一、题目 6462. 最小化字符串长度 - 力扣&#xff08;Leetcode&#xff09; 给你一个下标从 0 开始的字符串 s &#xff0c;重复执行下述操作 任意 次&#xff1a; 在字符串…

聊一聊数据库事务的那些事(隔离级别,传播行为)

我们平时使用事务的时候&#xff0c;可能脑子里面想到和事务有关的知识点无非就是&#xff0c;ACID&#xff0c;事务隔离级别那一套&#xff0c;使用的事务也就是是通过注解的形式&#xff0c;或者手动开启事务。更细致一点的问题或许没有深究下去&#xff0c;比如事务的传播行…

STM32F407的PWM

文章目录 32的PWM资源PWM输出原理捕获/比较模式寄存器&#xff08;TIMx_CCMR1/2&#xff09;捕获/比较使能寄存器&#xff08;TIMx_CCER&#xff09;捕获/比较寄存器&#xff08;TIMx_CCR1~4&#xff09; 库函数版本的PWM波输出开启 TIM3 时钟以及复用功能时钟置 &#xff0c;配…

对CT数据进行最小最大值归一化(Min-Max Normalization)和消除过暗过亮值处理

文章目录 PIL库的图像失真问题使用最小最大值归一化&#xff08;Min-Max Normalization&#xff09;预处理消除过暗过亮值pytorch中对tensor使用最小最大值归一化处理&#xff08;torchvision.transforms&#xff09; 我们在处理CT图像时&#xff08;以dcm格式为例&#xff09;…

全栈小程序开发路线

目录 个人心得&#xff1a; 我的学习路线 个人心得&#xff1a; 我擅长的是小程序开发和技术变现&#xff0c;从2021年至今开发上线20于个小程序&#xff0c;矩阵用户超过10万&#xff0c;变现10万左右。 以下是部分小程序截图&#xff0c;追风口做的小程序&#xff0c;基本…

「Win」Windows注册表介绍与操作

✨博客主页&#xff1a;何曾参静谧的博客 &#x1f4cc;文章专栏&#xff1a;「Win」Windows程序设计 相关术语 Windows的注册表&#xff1a;是一个重要的系统组件&#xff0c;用于存储操作系统和应用程序的配置信息。它类似于一个数据库&#xff0c;包含了各种键值对、参数、设…

Vue报错:Error: error:0308010C:digital envelope routines::unsupported解决

问题 node 环境 Node.js v18.14.2 使用npm start.出现以下报错 Error: error:0308010C:digital envelope routines::unsupported at new Hash (node:internal/crypto/hash:71:19) at Object.createHash (node:crypto:133:10) at module.exports (F:\RuoYi-Cloud\CourseSched…

SWAT模型系统学习(建模方法、实例应用、高级进阶)

目前&#xff0c;水环境问题逐渐成为制约社会经济和环境可持续发展的重要因素。根据国内外研究表明&#xff0c;受全球环境变化和经济快速发展的影响&#xff0c;面源污染已逐渐成为水环境污染的第一因素。但面源污染由于具有排放分散、隐蔽&#xff0c;排污随机、不确定、不易…

搜索算法(三) 回溯法

1.回溯法 回溯法可以理解成一种特殊的深度优先算法&#xff0c;比起普通的DFS&#xff0c;多了还原当前节点的一步。 修改当前节点、递归子节点、还原当前节点。 本质是一种试错的思想。 维基百科&#xff1a; 2.例题 1&#xff09; 力扣https://leetcode.cn/problems/pe…

树莓派安装系统

0. 实验准备 树莓派一个&#xff0c;TF卡&#xff08;4GB以上&#xff09;一个&#xff0c;读卡器一个 1. 使用官方提供的工具 在搜索引擎中搜索树莓派&#xff08;不要用百度&#xff0c;建议使用必应的国际版进行搜索&#xff09;&#xff0c;我这里直接放上树莓派官方超链…

深入篇【Linux】学习必备:理解【Linux软件包管理器】yum + yum的具体使用 + yum下载的有趣指令

这里写目录标题 Ⅰ.Linux软件包管理器yum①.什么是软件包/什么是yum②.linux的软件生态与yum源③.关于rzsz Ⅱ.yum基本指令①.查看软件②.安装软件③.卸载软件 Ⅲ.yum下载的好玩指令①.sl②.linux_logo③.elinks Ⅰ.Linux软件包管理器yum yum 是一个 Shell 前端软件包管理器。基…

C++ 多态 最详细图文+代码讲解

感谢各位 点赞 收藏 评论 三连支持 本文章收录于专栏【C进阶】 ❀希望能对大家有所帮助❀ 本文章由 风君子吖 原创 回顾 上篇文章&#xff0c;我们学习了继承的相关知识&#xff0c;详细解刨了继承中的各种细节&#xff0c;而本章内容将在继承的基础上学习多态 多态的概念…

Dreamweaver如何进行网页开发?

文章目录 0.引言1.安装Dreamweaver2.编写第一个网页 0.引言 笔者本科学习的编程语言主要是关于桌面开发的&#xff0c;对编程有一定的基础&#xff0c;而编程除了关于桌面软件开发&#xff08;VisualStudio如何进行桌面软件开发&#xff1f;&#xff09;&#xff0c;还有手机应…