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

Spring中每次访问数据库都要创建SqlSession吗?

    • 一、SqlSession是什么
    • 二、源码分析
      • 1)mybatis获取Mapper流程
      • 2)Spring创建Mapper接口的代理对象流程
      • 3)MapperFactoryBean#getObject调用时机
      • 4)SqlSessionTemplate创建流程
      • 5)SqlSessionInterceptor拦截逻辑
      • 6)开启事务后,关闭会话的时机分析
    • 三、总结

参考:https://www.zhihu.com/question/57568941/answer/2062846449

先来说结论:

  • 如果方法上标注了@Transactional 注解,则该方法里面多次访问数据库用的是同一个SqlSession(多线程调用除外)
  • 如果方法上没有标注该注解,则每访问一次数据库,都会创建新的SqlSession

一、SqlSession是什么

SqlSession是Mybatis工作的最顶层API会话接口,所有的数据库操作都经由它来实现。由于它是一个会话,即SqlSession对应这一次数据库会话,不是永久存活的,因此每次访问数据库时都需要创建它。

并且它不是线程安全的,如果将一个SqlSession搞成单例形式,或者静态域和实例变量的形式,都会导致SqlSession出现事务问题,这也就是为什么同一事务中的多个访问数据库请求会共用一个SqlSession会话,而不同事务则会创建不同SqlSession的原因。

SqlSession的创建过程:

  1. 从Configuration配置类中拿到Environment数据源;
  2. 从数据源中获取TransactionFactory和DataSource,并创建一个Transaction连接管理对象;
  3. 创建Executor对象(SqlSession只是所有操作的门面,真正要干活的是Executor,它封装了底层JDBC所有的操作细节);
  4. 创建SqlSession会话。

每次创建一个SqlSession会话,都会伴随创建一个专属SqlSession的连接管理对象,如果SqlSession共享,就会出现事务问题。

接下来的内容比较干,感兴趣的可以接着看!!!

二、源码分析

1)mybatis获取Mapper流程

先回顾以下传统mybatis创建Mapper接口的代理对象流畅如下:

  1. 如果没有引入spring的依赖,以前做法是通过sqlSession手动去获取Mapper对象,第一步是先创建SqlSession工厂对象,由它来创建SqlSession对象:
//sqlSessionFactory --> sqlSession
public class MybatisUtils {static SqlSessionFactory sqlSessionFactory = null;static {try {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);// 1.build方法会解析xml文件,包括我们写的mapper接口的xml文件,最终会把解析的信息封装到configuration对象中,// 特别是我们xml文件中的sql和相关信息都会被封装成一个个的MappedStatement对象存进一个Map中,key为全限定类名+方法名,value为MappedStatement对象,// 然后创建一个持有configuration引用的工厂对象返回,这里面就不展开分析了sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);} catch (IOException e) {e.printStackTrace();}}// 2.这里会创建一个持有configuration对象引用的DefaultSqlSession对象返回,并且Executor对象也是在这一步创建的,提供了在数据库执行 SQL 命令所需的所有方法,这里面不展开分析public static SqlSession getSqlSession(){return sqlSessionFactory.openSession();}
}

使用:

 	//1.获取SqlSession对象SqlSession sqlSession = MybatisUtils.getSqlSession();//2.获取代理对象,执行SQLUserDao userDao = sqlSession.getMapper(UserDao.class);List<User> userList = userDao.getUserList();for (User user : userList) {System.out.println(user);}//关闭sqlSessionsqlSession.close();
  1. 查看DefaultSqlSession的getMapper方法如下:
@Override
public <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);
}
  1. 接着会调用到MapperRegistry#getMapper方法如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {// 1.knownMappers会在解析mapper接口的xml文件时设置,key为接口的class对象,value为持有接口字节码对象引用的MapperProxyFactory对象final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {// 2.创建代理对象逻辑return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}}
  1. 接着调用MapperRegistry#newInstance 方法,MapperProxyFactory源码如下:
public class MapperProxyFactory<T> {private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}public Class<T> getMapperInterface() {return mapperInterface;}public Map<Method, MapperMethod> getMethodCache() {return methodCache;}@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {// 3.用JDK的方式去创建一个代理了mapper接口的代理对象返回,然后可以拿这个对象来执行增删改查查方法了,具体逻辑是现在MapperProxy中return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {// 1.创建MapperProxy对象,它持有sqlSession对象、接口字节码对象引用,并且它实现了InvocationHandler接口,这是动态代理的关键final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);// 2.创建mapper的代理对象return newInstance(mapperProxy);}
}
  1. 因为MapperProxy实现了InvocationHandler接口,所以代理对象调用方法时,会先经过MapperProxy#invoke方法
  private final Map<Method, MapperMethod> methodCache;@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 1.如果是父类Object的方法就直接反射调用if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (method.isDefault()) {// 2.如果是接口的默认方法,则调用invokeDefaultMethod方法return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);} // 3.我们自己mapper接口定义的方法,会接着调用MapperMethod#executefinal MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}// 4.如果mapperMethod 对象存在就不创建了,直接从缓存取private MapperMethod cachedMapperMethod(Method method) {// MapperMethod对象持有mapper接口字节码对象、要执行的目标方法对象、configuration对象引用return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));}
  1. MapperMethod#execute 方法如下:

     public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {// 根据全限定类名+方法名去获取MappedStatement对象,然后获取MappedStatement对象的标签类型,其中它的字段type为标签类型,name为全限定类名+方法名this.command = new SqlCommand(config, mapperInterface, method);this.method = new MethodSignature(config, mapperInterface, method);}public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}case SELECT:if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {result = executeForMap(sqlSession, args);} else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);} else {Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);if (method.returnsOptional()&& (result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);}}break;case FLUSH:result = sqlSession.flushStatements();break;default:throw new BindingException("Unknown execution method for: " + command.getName());}if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {throw new BindingException("Mapper method '" + command.getName()+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");}return result;}
    

这里面不具体展开了,大体逻辑是:

1)根据获取的MappedStatement对象的标签类型(对应xml文件的增删改查查标签:SELECT、DELETE、UPDATE、INSERT)来执行DefaultSqlSession对象对应的增删改查查方法

2)然后从configuration对象中根据全限定类名+方法名去获取一个MappedStatement对象

3)接着调用对应Executor对象的方法,并把MappedStatement对象传进去,Executor对象里面封装了JDBC的逻辑,以查询为例,大致逻辑为:

  • 首先会创建PreparedStatementHandler对象,接着会创建ParameterHandler 对象和ResultSetHandler对象,这些对象在创建时都会用InterceptorChain 拦截器链的pluginAll方法去判断是否需要增强这三个对象,如果要增强则会用动态代理来创建这些对象的代理对象,这也是mybatis插件原理的实现
  • PreparedStatementHandler 对象相当于JDBC的预编译语句对象,它会处理sgl语句预编译,设置参数等相关工作
  • 在设置预编译参数时(sql语句的占位符替换),PreparedStatementHandler 对象会调用ParameterHandlersetParameters方法来实现参数设置,里面会调用TypeHandler 对象方法来完成Java类型到数据库类型的转换
  • 在处理结果集时,PreparedStatementHandler 对象会调用ResultSetHandlerhandleResultSets方法来实现结果集映射,里面会调用TypeHandler 对象方法来完数据库类型到Java类型的转换

在这里插入图片描述

回顾了mybatis执行的大致原理,都是依靠DefaultSqlSession的方法,那引入了spring为什么就不需要我们手动创建sqlSession了呢,接下来接着分析

2)Spring创建Mapper接口的代理对象流程

  1. 当我们在接口标注一个@Mapper注解,并且@MapperScan注解的包路径能扫描到该接口时,则会对该接口生成一个工厂Bean对象MapperFactoryBean 放入一级缓存中:
// 省略其他代码...
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {private Class<T> mapperInterface;private boolean addToConfig = true;public MapperFactoryBean() {// intentionally empty}// 1.Mapper注解所在接口的字节码对象public MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}@Overrideprotected void checkDaoConfig() {super.checkDaoConfig();notNull(this.mapperInterface, "Property 'mapperInterface' is required");Configuration configuration = getSqlSession().getConfiguration();if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {try {configuration.addMapper(this.mapperInterface);} catch (Exception e) {logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);throw new IllegalArgumentException(e);} finally {ErrorContext.instance().reset();}}}// 2.注册真正的Mapper对象@Overridepublic T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}@Overridepublic Class<T> getObjectType() {return this.mapperInterface;}@Overridepublic boolean isSingleton() {return true;}public Class<T> getMapperInterface() {return mapperInterface;}
}
  1. 当Spring填充某个Bean的字段时,如果根据字段名称能从一级缓存获取到了Bean实例,并且该Bean实现了FactoryBean接口,则会调用该Bean的getObject方法,来获取真正的Bean来注入到对应字段中:
@Override
public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);
}
  1. 里面会调用父类的getSqlSession方法:
public abstract class SqlSessionDaoSupport extends DaoSupport {private SqlSessionTemplate sqlSessionTemplate;public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {// 2.如果sqlSessionTemplate为空,则创建该对象if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);}}// 3.创建SqlSessionTemplate对象,并把sqlSessionFactory对象传进去(持有Mapper.xml文件解析后的数据)@SuppressWarnings("WeakerAccess")protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}public final SqlSessionFactory getSqlSessionFactory() {return (this.sqlSessionTemplate != null ? this.sqlSessionTemplate.getSqlSessionFactory() : null);}public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {this.sqlSessionTemplate = sqlSessionTemplate;}// 1.获取SqlSessionTemplate对象,它实现了SqlSession接口public SqlSession getSqlSession() {return this.sqlSessionTemplate;}public SqlSessionTemplate getSqlSessionTemplate() {return this.sqlSessionTemplate;}@Overrideprotected void checkDaoConfig() {notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");}}

可以看到,getSqlSession方法最终会返回一个已经创建好的SqlSessionTemplate对象,它底层实现了SqlSession接口,并且每个MapperFactoryBean对象都会持有同一个SqlSessionTemplate 对象,因为它们都继承了同一个抽象父类SqlSessionDaoSupport 的sqlSessionTemplate字段

打断点发现一级缓存中的两个MapperFactoryBean对象确实持有相同的SqlSessionTemplate 引用,如下所示:

在这里插入图片描述

在这里插入图片描述

  1. 当返回SqlSessionTemplate对象之后,就会调用它的getMapper方法来获取Mapper接口的代理对象:
// 省略其他代码...
public class SqlSessionTemplate implements SqlSession, DisposableBean {public <T> T getMapper(Class<T> type) {// 2.关键部分:创建一个代理Mapper接口的对象返回return getConfiguration().getMapper(type, this);}// 1.获取创建时传进来的sqlSessionFactory对象中的Configuration对象@Overridepublic Configuration getConfiguration() {return this.sqlSessionFactory.getConfiguration();}}

在创建代理对象时,关键在于这个this引用是当前的SqlSessionTemplate对象,在前面的mybatis获取Mapper流程中分析了getMapper方法的逻辑,这里不在展开分析。

总之SqlSessionTemplate对象最终会被MapperProxy对象所持有,后续调用代理对象的方法时,都会由SqlSessionTemplate对象的方法来处理,所以我们引入Spring之后,会自动创建一个SqlSessionTemplate对象,由该对象代替mybatis手动创建的DefaultSqlSession来处理我们的增删改查查方法。

小结:

为什么引入Spring就不用手动去创建SqlSession对象了?

因为在注册MapperFactoryBean时,都会调用它的getObject方法,里面会返回一个实现了SqlSession接口的SqlSessionTemplate 对象,并且由会调用它的getMapper方法来获取代理Mapper接口的对象,其中实现了InvocationHandler 接口的MapperProxy 对象会持有SqlSessionTemplate 对象引用,最终调用代理对象的方法时,都会经过MapperProxy 的invoke方法来处理,具体是由SqlSessionTemplate 对象来处理的。

3)MapperFactoryBean#getObject调用时机

  1. 当我注入一个Mapper接口对象时,它会调用doGetBean方法,根据bean的名称从一级缓存中获取到对应的MapperFactoryBean对象:

在这里插入图片描述

  1. 来看看getObjectForBeanInstance 方法逻辑:
protected Object getObjectForBeanInstance(Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {if (BeanFactoryUtils.isFactoryDereference(name)) {if (beanInstance instanceof NullBean) {return beanInstance;}if (!(beanInstance instanceof FactoryBean)) {throw new BeanIsNotAFactoryException(beanName, beanInstance.getClass());}if (mbd != null) {mbd.isFactoryBean = true;}return beanInstance;}// 1.不是工厂Bean直接返回if (!(beanInstance instanceof FactoryBean)) {return beanInstance;}Object object = null;if (mbd != null) {mbd.isFactoryBean = true;}else {// 2.从缓存中获取代理Bean对象object = getCachedObjectForFactoryBean(beanName);}// 3.缓存中获取不到,说明第一次获取if (object == null) {FactoryBean<?> factory = (FactoryBean<?>) beanInstance;// Caches object obtained from FactoryBean if it is a singleton.if (mbd == null && containsBeanDefinition(beanName)) {mbd = getMergedLocalBeanDefinition(beanName);}boolean synthetic = (mbd != null && mbd.isSynthetic());// 4.里面会调用MapperFactoryBean#getObject方法object = getObjectFromFactoryBean(factory, beanName, !synthetic);}return object;}// 从缓存中获取代理Bean@Nullableprotected Object getCachedObjectForFactoryBean(String beanName) {return this.factoryBeanObjectCache.get(beanName);}// 缓存代理Beanprivate final Map<String, Object> factoryBeanObjectCache = new ConcurrentHashMap<>(16);
  1. 此时传进来的beanInstance是MapperFactoryBean实例,显然是工厂Bean对象,所以接下来会执行getObjectFromFactoryBean方法:
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {// 1.判断是不是单例,一级缓存中是否存在该工厂Bean对象,很显然是有的if (factory.isSingleton() && containsSingleton(beanName)) {synchronized (getSingletonMutex()) {// 2.再次从缓存中获取代理BeanObject object = this.factoryBeanObjectCache.get(beanName);if (object == null) {// 3.缓存还是没有,这下才回去调用MapperFactoryBean#getObject方法获取代理Bean对象object = doGetObjectFromFactoryBean(factory, beanName);Object alreadyThere = this.factoryBeanObjectCache.get(beanName);if (alreadyThere != null) {object = alreadyThere;}else {if (shouldPostProcess) {if (isSingletonCurrentlyInCreation(beanName)) {// Temporarily return non-post-processed object, not storing it yet..return object;}beforeSingletonCreation(beanName);try {// 4.这个方法最终会遍历所有的BeanPostProcessor,尝试执行postProcessAfterInitialization方法来对该代理Bean对象做后置增强,这里不在展开分析object = postProcessObjectFromFactoryBean(object, beanName);}catch (Throwable ex) {throw new BeanCreationException(beanName,"Post-processing of FactoryBean's singleton object failed", ex);}finally {afterSingletonCreation(beanName);}}if (containsSingleton(beanName)) {// 5.将增强后的代理Bean对象放入到缓存中,这样当别的类注入这个Mapper对象时,就不需要再走一遍后置增强的逻辑了。。直接从这个缓存获取即可this.factoryBeanObjectCache.put(beanName, object);}}}return object;}}else {Object object = doGetObjectFromFactoryBean(factory, beanName);if (shouldPostProcess) {try {object = postProcessObjectFromFactoryBean(object, beanName);}catch (Throwable ex) {throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);}}return object;}}
  1. doGetObjectFromFactoryBean方法:

可以看到这里面就会通过getObject方法来获取代理了我们Mapper接口的对象,并且它持有一个MybatisMapperProxy 引用(mybatis-plus框架的对象,也实现了InvocationHandler 接口),MybatisMapperProxy 对象里面又会持有SqlSessionTemplate对象的引用,假设没有引入mybatis-plus框架,最终代理对象持有的是MapperProxy 引用

在这里插入图片描述

这里的SqlSessionTemplate对象和前面图片的SqlSessionTemplate对象不同,是因为我重启了项目。。

小结:

  1. 当标注Mapper注解的接口被扫描到时,会生成一个该接口对应的MapperFactoryBean对象,然后将接口名称(第一个字母小写)作为key,MapperFactoryBean对象作为value放入到一级缓存中,注意此时还没有创建Mapper接口的代理对象
  2. 当我们给一些类的字段注入Mapper接口的对象时,此时会走getBean流程,根据接口名称从一级缓存获取到了MapperFactoryBean对象,接着Spring会判断该Bean是不是FactoryBean类型
  3. 如果该Bean不是FactoryBean类型,直接返回
  4. 如果该Bean是FactoryBean类型,此时会尝试从factoryBeanObjectCache这个缓存中根据接口名称获取Mapper接口的代理对象
  5. 如果获取到,说明该MapperFactoryBean对象已经生成了代理Bean对象,直接返回该代理Bean对象
  6. 如果获取不到,说明是第一次注入该Mapper接口的对象,则会去调用MapperFactoryBean的getObject方法来创建一个代理Mapper接口的对象返回
  7. 此时拿到的代理Bean对象还不能返回,会拿到所有的后置处理器尝试对该代理Bean对象做增强
  8. 将增强后的代理Bean对象放入到factoryBeanObjectCache缓存中,后续其他类就可以从这个缓存中注入该Bean对象了,不需要重复执行上面的步骤(Spring很多地方都用了这种本地缓存的思想),最后将该对象返回

4)SqlSessionTemplate创建流程

可以看到SqlSessionTemplate确实是实现了SqlSession接口

public class SqlSessionTemplate implements SqlSession, DisposableBean {// 持有Configuration对象引用private final SqlSessionFactory sqlSessionFactory;private final ExecutorType executorType;// 代理SqlSession接口的对象,也是SqlSessionTemplate的核心private final SqlSession sqlSessionProxy;private final PersistenceExceptionTranslator exceptionTranslator;//省略其他代码...
}

经过前面的分析,我们知道当调用代理Mapper接口的对象方法时,SqlSessionTemplate最终会代替DefaultSqlSession 来完成Mapper接口的增删改查查操作,所以我们先来看下SqlSessionTemplate 的创建流程:

  1. 调用有参构造方法,将sqlSessionFactory传进来:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());}public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {this(sqlSessionFactory, executorType,new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));}
  1. 最终会调用下面的构造方法:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");notNull(executorType, "Property 'executorType' is required");this.sqlSessionFactory = sqlSessionFactory;this.executorType = executorType;this.exceptionTranslator = exceptionTranslator;// 关键:是不是很熟悉这种代码,这里面创建了一个代理了SqlSession接口的对象,并且最终该代理对象的逻辑会被SqlSessionInterceptor拦截到,因为它实现了InvocationHandler接口this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),new Class[] { SqlSession.class }, new SqlSessionInterceptor());}

可以看到,它创建了一个代理SqlSession接口的对象,最终代理对象的方法都会被SqlSessionInterceptor拦截到

  1. 我们看下SqlSessionTemplate的一些其他方法:
@Overridepublic <E> List<E> selectList(String statement, Object parameter) {return this.sqlSessionProxy.selectList(statement, parameter);}@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {return this.sqlSessionProxy.selectList(statement, parameter, rowBounds);}@Overridepublic int insert(String statement) {return this.sqlSessionProxy.insert(statement);}@Overridepublic int update(String statement, Object parameter) {return this.sqlSessionProxy.update(statement, parameter);}@Overridepublic int delete(String statement, Object parameter) {return this.sqlSessionProxy.delete(statement, parameter);}

可以看到,当代理Mapper接口的对象执行增删改查查方法时,会被MapperProxy对象拦截到,然后由SqlSessionTemplate对象来处理,最终都会交由自己内部的sqlSessionProxy对象处理,而由于sqlSessionProxy也是个代理对象,它又会被SqlSessionInterceptor拦截来处理,所以接下来看下SqlSessionInterceptor做了什么处理,也是本篇文章问题的答案所在

5)SqlSessionInterceptor拦截逻辑

先来看下SqlSessionInterceptor的源码如下,它是SqlSessionTemplate的内部类:

private class SqlSessionInterceptor implements InvocationHandler {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1.获取SqlSession对象,见后面分析SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);try {// 2.执行DefaultSqlSession的方法Object result = method.invoke(sqlSession, args);// 3.判断是否开启了事务,见后面分析if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {// force commit even on non-dirty sessions because some databases require// a commit/rollback before calling close()// 4.没有开启,则提交事务sqlSession.commit(true);}return result;} catch (Throwable t) {Throwable unwrapped = unwrapThrowable(t);if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {// 5.关闭会话,见后面分析closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);// 6.置为null,finally块就不会重复执行closeSqlSession方法了sqlSession = null;Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);if (translated != null) {unwrapped = translated;}}throw unwrapped;} finally {if (sqlSession != null) {// 7.关闭会话closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);}}}
}

当我们执行Mapper接口的增删改查查方法时,最终都会执行到SqlSessionInterceptor的invoke方法,接下来分析下invoke方法的逻辑。

①获取SqlSession对象流程

  1. 首先会调用如下方法获取一个SqlSession对象:
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
  1. SqlSessionUtils#getSqlSession 方法如下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);// 1.从TransactionSynchronizationManager(以下称当前线程事务管理器)获取当前线程threadLocal是否有SqlSessionHolderSqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);// 2.如果获取到了,则从SqlSessionHolder中拿到SqlSession对象返回SqlSession session = sessionHolder(executorType, holder);if (session != null) {return session;}LOGGER.debug(() -> "Creating a new SqlSession");// 3.由SqlSessionFactory创建一个DefaultSqlSession对象,和使用mybaits手动创建DefaultSqlSession的方法一样session = sessionFactory.openSession(executorType);// 4.将SqlSession对象封装到SqlSessionHolder对象中,并保存到当前线程事务管理器中registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);return session;}

接下来依次分析getSqlSession中调用的方法

  1. TransactionSynchronizationManager#getResource方法流程:

先来看看当前线程事务管理器的结构:

public abstract class TransactionSynchronizationManager {// 存储当前线程事务资源,比如Connection、session等private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");// 存储当前线程事务同步回调器// 当有事务,该字段会被初始化,即激活当前线程事务管理器private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");// 省略其他代码...
}

TransactionSynchronizationManager#getResource方法如下:

public static Object getResource(Object key) {Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);// 根据SqlSessionFactory对象,从resources中获取SqlSessionHolder对象Object value = doGetResource(actualKey);if (value != null && logger.isTraceEnabled()) {logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +Thread.currentThread().getName() + "]");}return value;}

它会接着调用doGetResource方法:

@Nullableprivate static Object doGetResource(Object actualKey) {// 1.从resources中获取当前线程的事务资源Map<Object, Object> map = resources.get();if (map == null) {return null;}// 2.如果事务资源存在,则根据将SqlSessionFactory对象作为Key,去获取一个SqlSessionHolder对象Object value = map.get(actualKey);// Transparently remove ResourceHolder that was marked as void...if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {map.remove(actualKey);// Remove entire ThreadLocal if empty...if (map.isEmpty()) {resources.remove();}value = null;}// 3.返回SqlSessionHolder对象return value;}
  1. 当拿到SqlSessionHolder对象后,会执行sessionHolder方法来获取SqlSession对象:
private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {SqlSession session = null;if (holder != null && holder.isSynchronizedWithTransaction()) {if (holder.getExecutorType() != executorType) {throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");}holder.requested();LOGGER.debug(() -> "Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");// 从SqlSessionHolder中获取到SqlSession对象session = holder.getSqlSession();}return session;}
  1. 如果能从SqlSessionHolder中获取到SqlSession 对象,则直接返回,否则会执行下面的方法去创建一个DefaultSqlSession对象:
 session = sessionFactory.openSession(executorType);
  1. 当创建了SqlSession对象之后,会接着执行registerSessionHolder方法:
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {SqlSessionHolder holder;// 1.判断当前是否有事务if (TransactionSynchronizationManager.isSynchronizationActive()) {// 2.判断当前环境配置的事务管理工厂是否是SpringManagedTransactionFactory(默认)Environment environment = sessionFactory.getConfiguration().getEnvironment();if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]");// 3.创建一个SqlSessionHolder对象holder = new SqlSessionHolder(session, executorType, exceptionTranslator);// 4.绑定当前SqlSessionHolder到线程ThreadLocal中,即ThreadLocal<Map<Object, Object>> resources中TransactionSynchronizationManager.bindResource(sessionFactory, holder);// 5.注册SqlSession同步回调器到线程的本地变量synchronizations中TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));holder.setSynchronizedWithTransaction(true);// 会话引用次数+1holder.requested();} else {if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {LOGGER.debug(() -> "SqlSession [" + session+ "] was not registered for synchronization because DataSource is not transactional");} else {throw new TransientDataAccessResourceException("SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");}}} else {LOGGER.debug(() -> "SqlSession [" + session+ "] was not registered for synchronization because synchronization is not active");}}

注册SqlSession到当前线程事务管理器的条件首先是当前环境中有事务,否则不注册,判断是否有事务的条件是synchronizations的ThreadLocal是否为空:

public static boolean isSynchronizationActive() {return (synchronizations.get() != null);
}

每当我们开启一个事务(声明式、编程式),会调用initSynchronization()方法进行初始化synchronizations,以激活当前线程事务管理器:


private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =new NamedThreadLocal<>("Transaction synchronizations");public static void initSynchronization() throws IllegalStateException {if (isSynchronizationActive()) {throw new IllegalStateException("Cannot activate transaction synchronization - already active");}logger.trace("Initializing transaction synchronization");synchronizations.set(new LinkedHashSet<>());}

后续事务管理器AbstractPlatformTransactionManager 可以从synchronizations获取到SqlSessionHolder对象中的SqlSession来对事务管理,比如关闭Sqlsession。

②事务提交时机

当获取到SqlSession对象之后,接下来会执行以下方法:

Object result = method.invoke(sqlSession, args);// 判断有没有开启事务if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {// 提交当前事务sqlSession.commit(true);}

查看SqlSessionUtils#isSqlSessionTransactional方法如下:

public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {notNull(session, NO_SQL_SESSION_SPECIFIED);notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);// 从线程的本地变量中获取SqlSessionHolderSqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);// 如果SqlSessionHolder不为null说明开启了事务,返回truereturn (holder != null) && (holder.getSqlSession() == session);}

从前面的分析中,我们知道只有当开启事务时,才会将SqlSessionHolder 对象保存到线程的本地变量ThreadLocal<Map<Object, Object>> resources 中,所以如果没有开启事务的话,是不会保存的。

为什么要判断是否开启事务以控制当前事务提交?

例如:在一个方法上标注了@Transaction注解说明开启了事务,里面执行的方法都为增删改查的逻辑:

@Transactional(rollbackFor = Exception.class)
public void insertData(Item item, ItemStock itemStock) {itemStockMapper.save(itemStock);int i = 1 / 0;itemMapper.save(item);
}

如果没有这个判断逻辑,当该方法执行完itemStockMapper.save(itemStock),便会提交事务了,后面即使报错了也不会回滚了。正是因为有了这个判断,才不会出现这种情况,将标注@Transaction注解方法内的所有增删改查操作都看作一个整体事务,只有第一个增删改查方法执行时才会创建SqlSession对象,后续的每个增删改查方法执行时都能从线程的本地变量中获取到同一个SqlSession对象来使用,而只有当全部增删改查操作执行完成,才会提交事务。

那标注了@Transaction注解的方法是怎么提交的事务?

刚才看到了,只要没有开启事务并且没有报错,Spring会自动帮我们把事务提交了,这也就是为什么我们平常写代码不需要手动提交事务的原因。

而标注了@Transaction注解的提交事务时机又有所不同,这里不展开代码分析了,分析下大致逻辑:

  1. 当一个public方法被标注@Transaction 注解之后(后续简称目标方法),Spring会基于AOP给这个方法所在的类创建一个代理对象
  2. 并且会给这个代理对象,创建出一个方法拦截器TransactionInterceptor
  3. 假设这个代理对象是JDK代理的,那当我们执行这个代理对象的方法时,最终会执行到JdkDynamicAopProxy 的invoke方法
  4. 接着里面会根据方法对象和方法对象的hashcode去Map<MethodCacheKey, List<Object>> methodCache 这个缓存中尝试获取拦截器链
  5. 如果没有获取到,说明是第一次执行方法,则会从ProxyFactory (此对象在创建代理对象时会被保存在JdkDynamicAopProxy 中)获取增强器链
  6. 接着遍历增强链,如果不是方法拦截器则适配成方法拦截器,此时就获取到了TransactionInterceptor 这个拦截器对象
  7. 拿到拦截器链之后,就会按照顺序执行拦截器链中的拦截器方法以及目标方法
  8. 其中TransactionInterceptor 会比目标方法先执行,它会在目标方法执行之前开启事务,如果目标方法执行过程中报错,它会控制事务回滚,当目标方法执行完成之后,它才会控制事务提交。不过事务的处理是交由PlatformTransactionManager 这个事务管理器来处理的。

③closeSqlSession方法分析

无论是正常提交还是异常回滚,都会执行这个关闭会话的方法:

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {notNull(session, NO_SQL_SESSION_SPECIFIED);notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);// 1.从线程本地变量中获取SqlSessionHolder SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);if ((holder != null) && (holder.getSqlSession() == session)) {LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");// 2.能获取到,说明开启了事务,则不能关闭会话,减少会话引用次数holder.released();} else {LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");// 3.如果没有开启事务,则直接关闭会话session.close();}}

6)开启事务后,关闭会话的时机分析

在前面的分析中,当方法标注了@Transaction注解代表开了事务,则每次执行里面的子方法时都会从本地变量中获取到SqlSession对象,并且会话引用次数加一。在closeSqlSession方法逻辑中,只是将会话引用次数减一,并没有执行关闭会话的逻辑,那标注了@Transaction注解的方法什么时候才会关闭会话呢?

  1. 当方法标注@Transaction注解之后,Spring会给当前类生成一个代理对象,并且事务处理的拦截器为TransactionInterceptor ,所以当我们执行事务注解标注的方法时,假设没有异常的情况下,最终调用链路如下:

TransactionInterceptor#invokeTransactionAspectSupport#invokeWithinTransactionTransactionAspectSupport#commitTransactionAfterReturningAbstractPlatformTransactionManager#commitAbstractPlatformTransactionManager#processCommitAbstractPlatformTransactionManager#triggerBeforeCompletionSqlSessionSynchronization#beforeCompletion

  1. 首先会从前面分析获取SqlSession对象时synchronizations本地变量中获取到SqlSessionSynchronization 对象,里面保存了SqlSessionHolder对象,在整体事务提交之前会执行SqlSessionSynchronization 对象的 beforeCompletion 方法:
@Override
public void beforeCompletion() {// Issue #18 Close SqlSession and deregister it now// because afterCompletion may be called from a different thread// 1.判断会话引用次数是否大于0if (!this.holder.isOpen()) {// 2.小于等于0,说明@Transaction注解标注的方法里面的所有增删改查方法都执行完成了,可以进行会话关闭了LOGGER.debug(() -> "Transaction synchronization deregistering SqlSession [" + this.holder.getSqlSession() + "]");// 3.从线程的本地变量中移除SqlSessionHolderTransactionSynchronizationManager.unbindResource(sessionFactory);this.holderActive = false;LOGGER.debug(() -> "Transaction synchronization closing SqlSession [" + this.holder.getSqlSession() + "]");// 4.从SqlSessionHolder中获取SqlSession对象,执行会话关闭方法this.holder.getSqlSession().close();}
}// ResourceHolderSupport类的方法,方便查看
public boolean isOpen() {return (this.referenceCount > 0);}

因此,当我们开启事务之后,同一个事务的方法执行时,由于它们同属于一个SqlSession 会话,都会将会话引用次数加一,每个方法执行完成会将会话引用次数减一,当整个方法都执行完成之后,会话引用次数递减为0,最终Spring会判断会话引用次数是否大于0,如果大于0则不关闭会话,小于等于0才会关闭。

来个例子说明下:

@Transactional(rollbackFor = Exception.class)
public void insertData(Item item, ItemStock itemStock) {itemStockMapper.save(itemStock);itemMapper.save(item);
}
  1. 当执行insertData方法时,其实调用的是代理对象的方法,最终会被TransactionInterceptor拦截到,在目标对象的insertData执行前,会由AbstractPlatformTransactionManager 开启事务

  2. 当执行到itemStockMapper.save(itemStock)时,此时执行的也是代理了ItemStockMapper接口的对象方法,最终会执行到SqlSessionInterceptor 的invoke方法:

    • 前面也分析过了,第一次执行invoke方法,此时线程的本地变量没有SqlSessionHolder,所以会去创建SqlSession对象,并把它放入SqlSessionHolder对象中,接着会把SqlSessionHolder放入本地变量中,并对会话引用计数加一
    • 当itemStockMapper.save(itemStock)执行完成之后,此时会把会话引用计数减一,并没有提交事务
  3. 当执行到itemMapper.save(item)时,此时执行的也是代理了ItemMapper接口的对象方法,最终会执行到SqlSessionInterceptor 的invoke方法:

    • 这是第二次调用invoke方法,所以可以在本地变量中获取到SqlSession对象,并对会话引用计数加一
    • 当itemMapper.save(item)执行完成之后,此时会话引用计数减一变为0了,此时还没有提交事务
  4. 当整个insertData方法都执行完成之后,代表整个事务都完成了,此时会由AbstractPlatformTransactionManager 来提交事务,并且在提交的时候会判断会话引用计数是否大于0,如果小于等于0则关闭会话

三、总结

本文大致讲解了Mybatis手动创建SqlSession的流程,引入Spring之后为什么就不需要手动去创建SqlSession,以及Spring创建SqlSession的时机原理。

当我们引入Spring之后的处理:

  • 自动扫描标注@Mapper的接口,生成一个代理对象,其中代理对象的增删改查操作最终会由SqlSessionTemplate 来执行
  • SqlSessionTemplate 会生成一个代理SqlSession接口的对象,由该代理对象帮我们管理SqlSession的创建,当方法上标注了@Transactional 注解,则该方法里面多次访问数据库用的是同一个SqlSession,否则每次调用方法都会去创建一个SqlSession
  • TransactionInterceptor 会拦截标注@Transaction注解的方法,通过事务管理器PlatformTransactionManager 来对当前事务进行管理,包括正常提交、异常回滚、关闭会话等操作
  • 在开启事务后, SqlSessionHolder 对象起着很大重用,它保存着首次创建的SqlSession对象,并且它会存储在线程的两个本地变量中:
    • TransactionSynchronizationManager.resources :主要作用是给事务方法里面的多个子方法使用,保证了它们能从这个本地变量中获取同一个SqlSession对象(多线程情况除外)
    • TransactionSynchronizationManager.synchronizations :主要作用是由事务管理器AbstractPlatformTransactionManager从本地变量中获取SqlSession对象,来对全局事务进行管理。并且当它获取的值不为空时说明开启了事务,才会将SqlSessionHolder 对象保存在TransactionSynchronizationManager.resources

其实底层都是基于动态代理和AOP切面拦截的思想,通过这些机制和线程本地变量,让不同事务创建不同的SqlSession对象,让同一个事务共享同一个SqlSession对象,保证了线程安全。

最后再来一个例子:哪些方法会回滚?

在 insertData方法中里面调用了saveItem方法和saveItemStock方法,并且通过一个新线程调用了 saveItemStock,在 saveItemStock中抛出了异常,这些方法都开启了事务。

@Transactional
public void insertData(Item item, ItemStock itemStock) {itemStockService.saveItemStock(itemStock);new Thread(() -> {try {itemService.saveItem(item);} catch (Exception e) {throw new RuntimeException();}}).start();
}@Transactional
public void saveItemStock(ItemStock itemStock) {save(itemStock);throw new RuntimeException("111");
}@Transactional
public void saveItem(Item item) {save(item);
}

结果:saveItem不回滚、 saveItemStock 回滚:

  1. 这里相当于两个线程调用不同的事务方法,而每个线程不会共享自己的SqlSession
  2. saveItem无法回滚是因为没有捕获到新线程中抛出的异常
  3. saveItemStock方法可以回滚,是因为事务管理器只对当前线程中的事务有效

所以开启事务后,在多线程环境下事务管理器并不会跨线程传播事务,事务的状态是存储在线程的本地ThreadLocal 中, 方便后续管理当前线程的事务上下文信息。这也意味着每个线程都有一个独立的事务上下文,事务信息在不同线程之间不会共享。

这篇文章断断续续写了好几天,再加上自己的表达能力有限,所以写起来有点乱,见谅见谅,如果有错误的地方欢迎指正!

相关文章:

Spring中每次访问数据库都要创建SqlSession吗?

一、SqlSession是什么二、源码分析1&#xff09;mybatis获取Mapper流程2&#xff09;Spring创建Mapper接口的代理对象流程3&#xff09;MapperFactoryBean#getObject调用时机4&#xff09;SqlSessionTemplate创建流程5&#xff09;SqlSessionInterceptor拦截逻辑6&#xff09;开…...

【数据分析】布朗运动(维纳过程)

文章目录 一、概述二、数学布朗运动2.1 数学定义2.2 布朗运动的数学模型2.21 标准布朗运动2.22 布朗运动的路径2.23 布朗运动的方程 三、布朗运动在金融学中的应用四、数学构造&#xff08;以傅里叶级数为例&#xff09;4.1 傅里叶级数的基本思想4.2 构造布朗运动 一、概述 布…...

静态页面 和 动态页面(Java Web开发)

1. 静态页面 1.1 什么是静态页面&#xff1f; 静态页面是指 HTML 文件直接存放在服务器上&#xff0c;不依赖后端逻辑处理而生成内容。客户端浏览器请求静态页面时&#xff0c;服务器直接将文件发送到客户端&#xff0c;浏览器负责渲染页面。 特点&#xff1a; 固定内容&am…...

linux模拟试题

Linux 基础阶段考试笔试模拟试卷 审核人:王旺旺 一.填空题(每题 1 分,共 30 分) 1.验证 httpd 服务是否启动的命令是_______ 答:systemctl status httpd 或 netstat -anptl 或 ss -anpt 2.将目录 xxhf 下所有文件的所属组改为 user1 的命令是_______ 答:chown -R ,user1 …...

Android 使用OpenGLES + MediaPlayer 获取视频截图

概述 Android 获取视频缩略图的方法通常有: ContentResolver: 使用系统数据库MediaMetadataRetriever: 这个是android提供的类&#xff0c;用来获取本地和网络media相关文件的信息ThumbnailUtils: 是在android2.2&#xff08;api8&#xff09;之后新增的一个&#xff0c;该类为…...

典型的1553B网络

典型的1553B网络 1553B总线BUS A和BUS B是互为冗余的、完全对等、物理隔离的两个网络。每一个节点设备也配置有互为冗余的A、B两个1553B接口&#xff0c;分别接入BUS A和BUS B。系统完成初始化配置后&#xff0c;首先采用BUS A来通讯。工作过程中&#xff0c;如果发现BUS A的工…...

【C++】内存管理

【C】内存管理 一、C/C内存分布二、C语言中动态内存管理方式三、C内存管理方式1、new 和 delete 操作内置类型2、new 和 delete 操作自定义类型 四、operator new 和 operator delete 函数五、new 和 delete 的实现原理1、内置类型2、自定义类型3、new和delete不匹配的报错 六、…...

实现PDF文档加密,访问需要密码

01. 背景 今天下午老板神秘兮兮的来问我&#xff0c;能不能做个文档加密功能&#xff0c;就是那种用户下载打开需要密码才能打开的那种效果。boss都发话了&#xff0c;那必须可以。 需求&#xff1a;将 pdf 文档经过加密处理&#xff0c;客户下载pdf文档&#xff0c;打开文档需…...

常见排序算法总结 (三) - 归并排序与归并分治

归并排序 算法思想 将数组元素不断地拆分&#xff0c;直到每一组中只包含一个元素&#xff0c;单个元素天然有序。之后用归并的方式收集跨组的元素&#xff0c;最终形成整个区间上有序的序列。 稳定性分析 归并排序是稳定的&#xff0c;拆分数组时会自然地将元素分成有先后…...

文库 | 从嬴图的技术文档聊起

在技术的浩瀚海洋中&#xff0c;一份优秀的技术文档宛如精准的航海图。它是知识传承的载体&#xff0c;是团队协作的桥梁&#xff0c;更是产品成功的幕后英雄。然而&#xff0c;打造这样一份出色的技术文档并非易事。你是否在为如何清晰阐释复杂技术而苦恼&#xff1f;是否纠结…...

故障诊断 | Transformer-LSTM组合模型的故障诊断(Matlab)

效果一览 文章概述 故障诊断 | Transformer-LSTM组合模型的故障诊断(Matlab) 源码设计 %% 初始化 clear close all clc disp(此程序务必用2023b及其以上版本的MATLAB!否则会报错!) warning off %...

VScode离线下载扩展安装

在使用VScode下在扩展插件时&#xff0c;返现VScode搜索不到插件&#xff0c;网上搜了好多方法&#xff0c;都不是常规操作&#xff0c;解决起来十分麻烦&#xff0c;可以利用离线下载安装的方式安装插件&#xff01;亲测有效&#xff01;&#xff01;&#xff01; 1.找到VScod…...

【AI系统】昇腾异构计算架构 CANN

昇腾异构计算架构 CANN 本文将介绍昇腾 AI 异构计算架构 CANN&#xff08;Compute Architecture for Neural Networks&#xff09;&#xff0c;这是一套为高性能神经网络计算需求专门设计和优化的架构。CANN 包括硬件层面的达芬奇架构和软件层面的全栈支持&#xff0c;旨在提供…...

云服务器重装系统后 一些报错与解决[ vscode / ssh / 子用户]

碰见的三个问题&#xff1a; 1.vscode连接失败 2.登录信息配置 3.新建子用户的一些设置 思考&#xff1a;遇见问题&#xff0c;第一反应 应该如何解决 目录 1. 错误 解决方法 原因 步骤 1&#xff1a;找到known_hosts文件并编辑 步骤 2&#xff1a;通过VSCode终端输入…...

架构设计之路,永无尽头

1. 插件式架构 2. SRP:单一职责原则 3. 链接加载器&#xff1f;&#xff1f;&#xff1f; 4. 端口适配器架构 5. 六边形架构 6. MVC架构 7. 领域驱动架构 8. 敏捷开发 9. 打台球的时候每打一杆是为了下几杆&#xff0c;而不是为了打到洞中。 10. 画出一个图&#xff0…...

【AI系统】Ascend C 语法扩展

Ascend C 语法扩展 Ascend C 的本质构成其实是标准 C加上一组扩展的语法和 API。本文首先对 Ascend C 的基础语法扩展进行简要介绍&#xff0c;随后讨论 Ascend C 的两种 API——基础 API 和高阶 API。 接下来针对 Ascend C 的几种关键编程对象——数据存储、任务间通信与同步…...

驱动篇的开端

准备 在做之后的动作前&#xff0c;因为win7及其以上的版本默认是不支持DbgPrint&#xff08;大家暂时理解为内核版的printf&#xff09;的打印&#xff0c;所以&#xff0c;为了方便我们的调试&#xff0c;我们先要修改一下注册表 创建一个reg文件然后运行 Windows Registr…...

树莓派4B使用opencv读取摄像头配置指南

本文自己记录&#xff0c;给我们lab自己使用&#xff0c;其他朋友们不一定完全适配&#xff0c;请酌情参考。 一. 安装opecnv 我们的树莓派4B默认是armv7l架构&#xff0c;安装的miniconda最新的版本 Miniconda3-latest-Linux-armv7l.sh 仍然是python3.4几乎无法使用&#xff…...

【AI日记】24.12.03 kaggle 比赛 Titanic-6

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 工作 内容&#xff1a;学习 kaggle 入门比赛 Titanic - Machine Learning from Disaster时间&#xff1a;7 小时评估&#xff1a;继续 读书 书名&#xff1a;美丽新世界时间&#xff1a;1 小时阅读原因&…...

Linux中的常用基本指令(下)

Linux常用基本指令 Linux中的基本指令12.head指令13.tail指令简单解释重定向与管道(重要) 14.date指令(时间相关的指令)15.cal指令(不重要)16.find指令(灰常重要&#xff09;17.grep指令(重要)18.which指令和alias指令19.zip/unzip指令&#xff1a;20.tar指令&#xff08;重要&…...

python笔记3

复习及总结 python的软件安装及简单使用——python3.31 pycharm python的输出&#xff1a;print&#xff08;&#xff09; 简单&#xff08;直接&#xff09;输出 print&#xff08;&#xff09;输出到指定文件 fpopen(rC:\Users\M15R3\Desktop\1.txt,a) print("334…...

电商营销活动-抽奖业务

目录 一、抽奖系统的核心功能 二、抽奖系统的业务逻辑 三、抽奖系统的业务优势 四、抽奖系统的业务注意事项 电商营销活动中的抽奖系统业务&#xff0c;是一种通过设立抽奖活动来吸引用户参与、提升用户活跃度和转化率的营销手段。以下是对电商营销活动抽奖系统业务的详细解…...

利用 Redis 与 Lua 脚本解决秒杀系统中的高并发与库存超卖问题

1. 前言 1.1 秒杀系统中的库存超卖问题 在电商平台上&#xff0c;秒杀活动是吸引用户参与并提升销量的一种常见方式。秒杀通常会以极低的价格限量出售某些商品&#xff0c;目的是制造紧迫感&#xff0c;吸引大量用户参与。然而&#xff0c;这种活动的特殊性也带来了许多技术挑…...

《山海经》:北山

《山海经》&#xff1a;北山 北山一经单狐山求如山&#xff08;水马&#xff1a;形状与马相似&#xff0c;滑鱼&#xff1a;背部红色&#xff09;带山&#xff08;䑏疏&#xff1a;似马&#xff0c;一只角&#xff0c;鵸鵌&#xff1a;状乌鸦五彩斑斓&#xff0c;儵鱼&#xff…...

React基础教程(12):useRef的使用

12、useRef useRef 是 React 中的一个 Hook,主要用于访问和操作 DOM 元素以及保存组件的可变引用值。它是一个工具,用来避免重新渲染组件的情况下保持某些状态或引用的值。 使用场景: 使用场景 访问 DOM 元素 当需要直接操作某个 DOM 元素(如聚焦、滚动等)时,可以使用…...

释放超凡性能,打造鸿蒙原生游戏卓越体验

11月26日在华为Mate品牌盛典上&#xff0c;全新Mate70系列及多款全场景新品正式亮相。在游戏领域&#xff0c;HarmonyOS NEXT加持下游戏的性能得到充分释放。HarmonyOS SDK为开发者提供了软硬协同的系统级图形加速解决方案——Graphics Accelerate Kit&#xff08;图形加速服务…...

Linux--Debian或Ubuntu上扩容、挂载磁盘并配置lvm

一、三块12TB组RAID 5 可用容量约24TB 二、安装LVM工具&#xff08;已安装请忽略&#xff09; sudo apt-get install lvm2二、查看可用磁盘 sudo lsblk 或者 sudo fdisk -l三、创建物理卷&#xff08;PV&#xff09; 选中刚做的磁盘组 sudo pvcreat /dev/sdb1四、创建卷组…...

我谈冈萨雷斯对频域滤波的误解——快速卷积与频域滤波之间的关系

在Rafael Gonzalez和Richard Woods所著的《数字图像处理》中&#xff0c;Gonzalez对频域滤波是有误解的&#xff0c;在频域设计滤波器不是非得图像和滤波器的尺寸相同&#xff0c;不是非得在频域通过乘积实现。相反&#xff0c;FIR滤波器设计都是构造空域脉冲响应。一般的原则是…...

Leetcoed:3274

1&#xff0c;题目 2&#xff0c;思路 把俩个字符串坐标拆开比较二进制, 如a1与b2 ,a与b比较为false ,1与2比较为false,最后俩个结果比较返回true 3&#xff0c;代码 class Solution3274 {public boolean checkTwoChessboards(String str1, String str2) {return (str1.char…...

LabVIEW实现串口调试助手

目录 1、串口通信原理 2、硬件环境部署 3、串口通信函数 4、程序架构 5、前面板设计 6、程序框图设计 本专栏以LabVIEW为开发平台,讲解物联网通信组网原理与开发方法,覆盖RS232、TCP、MQTT、蓝牙、Wi-Fi、NB-IoT等协议。 结合实际案例,展示如何利用LabVIEW和常用模块实现物联…...

ASP.NET Core项目中使用SqlSugar连接多个数据库的方式

之前学习ASP.NETCore及SqlSugar时都是只连接单个数据库处理数据&#xff0c;仅需在Program文件中添加ISqlSugarClient的单例即可&#xff08;如下代码所示&#xff09;。 builder.Services.AddSingleton<ISqlSugarClient>(s > {SqlSugarScope sqlSugar new SqlSugar…...

leetcode hot100【Leetcode 72.编辑距离】java实现

Leetcode 72.编辑距离 题目描述 给定两个单词 word1 和 word2&#xff0c;返回将 word1 转换为 word2 所使用的最少操作数。 你可以对一个单词执行以下三种操作之一&#xff1a; 插入一个字符删除一个字符替换一个字符 示例 1: 输入: word1 "horse", word2 &…...

【开源免费】基于Vue和SpringBoot的服装生产管理系统(附论文)

博主说明&#xff1a;本文项目编号 T 066 &#xff0c;文末自助获取源码 \color{red}{T066&#xff0c;文末自助获取源码} T066&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析…...

Android13 允许桌面自动旋转

一&#xff09;需求-场景 Android13 实现允许桌面自动旋转 Android13 版本开始后&#xff0c;支持屏幕自动旋转&#xff0c;优化体验和兼容性&#xff0c;适配不同屏幕 主界面可自动旋转 二&#xff09;参考资料 android framework13-launcher3【06手机旋转问题】 Launcher默…...

异常知识及其使用

异常的简单概念 在C中&#xff0c;异常处理是一种机制&#xff0c;用于处理程序运行时发生的意外情况。它允许程序在发生错误时&#xff0c;将控制权转移到一个专门的代码块&#xff0c;而不是让程序直接崩溃。C的异常处理机制包括以下几个关键概念&#xff1a; throw 用途&…...

Spark常问面试题---项目总结

一、数据清洗&#xff0c;你都清洗什么&#xff1f;或者说 ETL 你是怎么做的&#xff1f; 我在这个项目主要清洗的式日志数据&#xff0c;日志数据传过来的json格式 去除掉无用的字段&#xff0c;过滤掉json格式不正确的脏数据 过滤清洗掉日志中缺少关键字段的数据&#xff…...

哈希及其模拟实现

1.哈希的概念 顺序结构以及平衡树中&#xff0c;元素的关键码与其存储位置之间没有对应的关系。因此&#xff0c;在查找一个元素时&#xff0c;必须要经过关键码的多次比较。顺序查找的时间复杂度为O(N)&#xff0c;平衡树中为树的高度&#xff0c;即O(log_2 N)&#xff0c;搜…...

Day 32 动态规划part01

今天正式开始动态规划! 理论基础 无论大家之前对动态规划学到什么程度,一定要先看 我讲的 动态规划理论基础。 如果没做过动态规划的题目,看我讲的理论基础,会有感觉 是不是简单题想复杂了? 其实并没有,我讲的理论基础内容,在动规章节所有题目都有运用,所以很重要!…...

【娱乐项目】竖式算术器

Demo介绍 一个加减法随机数生成器&#xff0c;它能够生成随机的加减法题目&#xff0c;并且支持用户输入答案。系统会根据用户输入的答案判断是否正确&#xff0c;统计正确和错误的次数&#xff0c;并显示历史记录和错题记录。该工具适合用于数学练习&#xff0c;尤其适合练习基…...

XRP 深度解析:从技术到 Meme 币交易指南

撰文&#xff1a;Ignas | DeFi Research 编译&#xff1a;Yuliya&#xff0c;PANews 本文来源Techub News:XRP 深度解析&#xff1a;从技术到 Meme 币交易指南 在当前加密货币市场&#xff0c;一个令人瞩目的现象正在上演&#xff1a;XRP 在短短一个月内暴涨 3.5 倍&#xf…...

机器学习周志华学习笔记-第13章<半监督学习>

机器学习周志华学习笔记-第13章&#xff1c;半监督学习&#xff1e; 卷王&#xff0c;请看目录 13半监督学习13.1 生成式方法13.2 半监督SVM13.3 基于分歧的方法13.4 半监督聚类 13半监督学习 前面我们一直围绕的都是监督学习与无监督学习&#xff0c;监督学习指的是训练样本包…...

【MySql】navicat连接报2013错误

navicat连接mysql报2013错误 报错信息1、检验Mysql数据库是否安装成功2、对Mysql的配置文件进行修改配置2.1、找到配置文件2.2、Linux下修改配置文本 3、连接进入mysql服务4、在mysql下执行授权命令 报错信息 Navicat连接mysql报2013错误 2013-Lost connection to MYSQL serve…...

【微服务】Docker

一、Docker基础 1、依赖的兼容问题&#xff1a;Docker允许开发中将应用、依赖、函数库、配置一起打包&#xff0c;形成可移植镜像Docker应用运行在容器中&#xff0c;使用沙箱机制&#xff0c;相互隔离。 2、如何解决开发、测试、生产环境有差异的问题&#xff1a;Docker镜像…...

renderExtraFooter 添加本周,本月,本年

在 Ant Design Vue 中&#xff0c;a-date-picker 组件提供了一个 renderExtraFooter 属性&#xff0c;可以用来渲染额外的页脚内容。你可以利用这个属性来添加“本周”、“本月”和“本年”的按钮。下面是如何在 Vue 2 项目中实现这一功能的具体步骤&#xff1a; 1.确保安装了…...

警惕开源信息成为泄密源头

文章目录 前言一、信息公开需谨慎1、警惕采购招标泄密。2、警惕信息公开泄密。3、警惕社交媒体泄密。 二、泄密风险需严防1、健全制度&#xff0c;明确责任。2、加强管控&#xff0c;严格审查。3、提高意识&#xff0c;谨言慎行。 前言 大数据时代&#xff0c;信息在网络空间发…...

密码学和CA证书

参考视频 一. 公钥私钥的理解 我们提到的使用公钥私钥进行加密解密只是一种口头表达方式&#xff0c;准确来说应该是公钥和私钥通过加密 算法生成&#xff0c;也需要通过配合加密算法进行解密。而不是直接用公钥和私钥进行加密解密。 二. 对称加密和非对称加密算法 1. 非对…...

Python 入门教程(2)搭建环境 | 2.4、VSCode配置Node.js运行环境

文章目录 一、VSCode配置Node.js运行环境1、软件安装2、安装Node.js插件3、配置VSCode4、创建并运行Node.js文件5、调试Node.js代码 一、VSCode配置Node.js运行环境 1、软件安装 安装下面的软件&#xff1a; 安装Node.js&#xff1a;Node.js官网 下载Node.js安装包。建议选择L…...

Nginx Web服务器管理、均衡负载、访问控制与跨域问题

Nginx Web 服务器的均衡负载、访问控制与跨域问题 Nginx 的配置 1. 安装Nginx 首先安装Nginx apt install nginx -ycaccpurgatory-v:~$ sudo apt install nginx [sudo] password for cacc: Reading package lists... Done Building dependency tree... Done Reading state i…...

排序学习整理(2)

上集回顾 排序学习整理&#xff08;1&#xff09;-CSDN博客 2.3 交换排序 交换排序的基本思想是&#xff1a;根据序列中两个记录键值的比较结果&#xff0c;交换这两个记录在序列中的位置。 特点&#xff1a; 通过比较和交换操作&#xff0c;将键值较大的记录逐步移动到序列…...

【前端】将vue的方法挂载到window上供全局使用,也方便跟原生js做交互

【前端】将vue的方法挂载到window上供全局使用&#xff0c;也方便跟原生js做交互 <template><div><el-button click"start">调用方法</el-button></div> </template> <script> // import { JScallbackProc } from ./JScal…...