mybatis的一级二级缓存详解及源码解剖

news2024/12/22 13:36:12

文章目录

  • 什么是一级缓存?
  • 什么是二级缓存?
  • 一级缓存二级缓存有什么不同?
  • 执行流程
  • 源码流程解剖
  • 一级缓存失效场景分析
  • 二级缓存结构及需要解决的问题
  • 二级缓存执行流程
  • 二级缓存获取和commit源码解剖
  • 总结


什么是一级缓存?

一级缓存是指在同一个SqlSession中,对于相同的查询语句和参数,第一次查询的结果会被缓存到内存中,后续的查询会直接从缓存中获取结果,而不会再次查询数据库。一级缓存是MyBatis默认开启的,可以通过在SqlSession中调用clearCache()方法来清空缓存。


什么是二级缓存?

二级缓存是指在多个SqlSession中,对于相同的查询语句和参数,第一次查询的结果会被缓存到内存中,后续的查询会直接从缓存中获取结果,而不会再次查询数据库。二级缓存是需要手动开启的,可以通过在Mapper.xml文件中添加标签来开启。二级缓存的作用范围是Mapper级别的,也就是说,同一个Mapper.xml文件中的查询语句会共享同一个缓存。


一级缓存二级缓存有什么不同?

在这里插入图片描述

一级缓存和二级缓存的不同点在于作用范围和生命周期。一级缓存的作用范围是SqlSession级别的,生命周期也是和SqlSession一样的,当SqlSession关闭时,缓存也会被清空。而二级缓存的作用范围是Mapper级别的,生命周期是和应用程序一样的,当应用程序关闭时,缓存也会被清空。另外,二级缓存需要手动开启,而一级缓存是默认开启的。


执行流程

在这里插入图片描述

  public void testMybatis()throws Exception{
		
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder=new SqlSessionFactoryBuilder();
    org.springframework.core.io.ClassPathResource classPathResource=new ClassPathResource("org/apache/ibatis/user/mybatis.xml");
    InputStream inputStream = classPathResource.getInputStream();
    // 1.读取配置文件获取SqlSessionFactory 
    SqlSessionFactory sqlSessionFactory= sqlSessionFactoryBuilder.build(inputStream);
    // 2.获取sqlsession
    SqlSession sqlSession = sqlSessionFactory.openSession();
	
	// 3.执行获取结果
    User user = new User(1L, null, null, null);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> users = mapper.selectUser(user);
    mapper.selectUser(user);
    System.out.println(users);
    }

CachingExecutor 二级缓存执行器

CachingExecutor类直接实现了Excutor接口,是装饰器类,主要用来增强缓存相关功能。在CachingExecutor类中,为了完成缓存相关功能,需要TransactionalCacheManager和TransactionalCache两个类的支持,其中TransactionalCacheManager类主要用来管理缓存数据的对象TransactionalCache,而TransactionalCache对象是用来真正缓存数据的对象。

BaseExecutor 一级缓存执行器

BaseExecutor是MyBatis中所有Executor的基类,它实现了Executor接口中的一些通用方法,例如query()、update()、flushStatements()等。BaseExecutor中定义了一个MappedStatement和CacheKey的Map,用于缓存已经解析过的MappedStatement和CacheKey对象,以提高查询效率。BaseExecutor还定义了一个Transaction对象,用于管理事务的提交和回滚。

SimpleExecutor(默认是SimpleExecutor)每次执行SQL语句都需要进行编译

SimpleExecutor是最简单的Executor实现,每次执行SQL语句时都会创建一个新的Statement对象,执行完毕后立即关闭Statement对象。SimpleExecutor适用于短时间内需要执行大量SQL语句的场景,但是由于每次都需要创建和关闭Statement对象,所以效率相对较低。

ReuseExecutor 执行相同SQL语句时,如果之前SQL已编译,则不会在进行编译。

ReuseExecutor是在SimpleExecutor的基础上进行了优化,它会在执行SQL语句之前先检查是否已经存在可重用的Statement对象,如果存在则直接使用,否则创建一个新的Statement对象。ReuseExecutor适用于需要执行多个相同的SQL语句的场景,可以减少创建和关闭Statement对象的次数,提高效率。

BatchExecutor 批量执行

BatchExecutor是用于批量执行SQL语句的Executor实现,它会将多个SQL语句合并成一个批量操作,然后一次性发送给数据库执行。BatchExecutor适用于需要执行大量相似的SQL语句的场景,可以减少网络传输和数据库操作的次数,提高效率。

在MyBatis中,可以通过在配置文件中设置defaultExecutorType属性来指定默认的Executor实现,也可以在Mapper.xml文件中通过executorType属性来指定特定的Executor实现。


源码流程解剖

一:获取UserMapper

我们先来看看 UserMapper mapper = sqlSession.getMapper(UserMapper.class);这一行代码返回的是一个什么对象。
在这里插入图片描述

1.首先我们可以看到,mybatis通过动态代理模式给我们生成了一个Usermapper对象。

2.在这个mapper对象当中,有一个sqlSession对象,SqlSession里面有一个executor,这个executor是CachingExecutor(这个是二级缓存的CachingExecutor)

3.在CachingExecutor中有一个delegate它是一个SimpleExecutor。(这里就要注意了,因为BaseExecutor是SimpleExecutor的父类,只不过mybatis默认是SimpleExecutor。所以这里delegate其实是指向了一级缓存BaseExecutor)

二:执行UserMapper.selectUser(user)的查询方法

由于我们的UserMapper是动态代理生成的代理对象,那么在执行方法前会先去执行invoke方法

List<User> users = mapper.selectUser(user);

MapperProxy代理对象

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      }
      // 这个就是调用mapperMethod.execute方法,看当前查询是select、update、delete等等
      // 在根据具体的实现去调用DefaultSqlSession里面的(selectList()、insert()等等)
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

DefaultSqlSession,因为我们查询的是一个数组,所以就调用了selectList()方法

  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
     // 1. 获取一个statement
      MappedStatement ms = configuration.getMappedStatement(statement);
      dirty |= ms.isDirtySelect();
      // 2.执行二级缓存CachingExecutor的query方法
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

CachingExecutor 二级缓存的query方法

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
      throws SQLException {
    // 这就是解析你的SQL,把SQL解析为 select * from user wehre id=?
    // 并且包含了SQL语句和参数列表等信息
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // CacheKey对象中包含了MappedStatement的ID、查询参数、分页参数等信息,这个比较重要。后面会详细讲解
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

CachingExecutor 二级开始干事情的query方法

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
     // 1.获取当前mappernamespace是否配置了<cache/>标签,是否开启了二级缓存 ,从二级缓存空气获取该cache
    Cache cache = ms.getCache();
    if (cache != null) {
    	// 判断是否要清除缓存,只会清空当前线程的缓存,不会影响其他线程的缓存。
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
       // 检查SQL语句中是否包含输出参数
        ensureNoOutParams(ms, boundSql);
        // 从二级缓存中获取结果
        List<E> list = (List<E>) tcm.getObject(cache, key);
        
        // 从一级缓存中获取结果并将结果存储到二级缓存map当中
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        //  从二级缓存中拿出结果,进行返回
        return list;
      }
    }
    // delegate是我们的一级缓存,如果没有开启二级缓存,就直接调用一级缓存获取结果进行返回
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

BaseExecutor的query方法

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 从一级缓存中获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
      // 处理本地缓存中的输出参数,不会对SQL语句进行解析或执行
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      	// 一级缓存没有就去数据库查询结构,后面会调用SimpleExecutor的doQuery方法。 
      	// 而且会把查询数据存入一级缓存当中
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    // 清除刷新缓存逻辑等等
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

SimpleExecutor的doQuery

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
          boundSql);
       // 获取连接 ,会调用PooledDataSource.getConnection()
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 查询数据库
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

StatementHandler.query最后会调用PreparedStatementHandler的query方法

// 看到这里大家就非常熟悉了,和jdbc一样PreparedStatement 执行SQL语句进行返回。
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

一级缓存失效场景分析

在上面我们从一级缓存获取数据时,都是通过key来获取的。我们来看看这个key到底是啥。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 从一级缓存中获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
      // 处理本地缓存中的输出参数,不会对SQL语句进行解析或执行
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
    
  }

从一级缓存获取数据
list = resultHandler == null ? (List) localCache.getObject(key) : null;

这个CacheKey key,前面我也标注了是在二级缓存调用query方法时获取的。那我们来看看这个key里面有啥玩意
在这里插入图片描述
在updateList中有6参数我们来依次看看啥意思。
参数 0:指向的就是我们statementID
参数1:分页数值
参数2:分页数值
参数3:编译后的SQL语句
参数4:设置的参数值
参数5:就是你配置数据源时的default值

<environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value=""/>
        <property name="username" value=""/>
        <property name="password" value=""/>
      </dataSource>
    </environment>
  </environments>

那么我们就可以总结一级缓存有哪些失效场景了。(修改,新增操作就忽略)

  1. 必须在同一个statementID的语句中执行
  2. SQL语句和参数值必须一致。
  3. 分页条件必须一致
  4. 必须在同一个会话中,在上面代码中我们也可以看到。我们调用mapper.selectUser(user);是会执行sqlSession的excutor方法。如果sqlSession关闭了,那么里面的一级执行器,当然也就不会在存在了。

这里可能有疑问,sqlSession什么时候销毁呢?

戳这里,在spring中sqlSession的创建及销毁


二级缓存结构及需要解决的问题

首先我们需要考虑的是,既然二级缓存是可以在不同的sqlSession进行数据共享,那么会有哪些问题需要我们去解决。

问题一:线程安全问题,既然二级缓存可以跨线程使用。那么我们是不是要考虑线程安全、脏读等问题。

问题二:既然是缓存,不是存储在内存中,就是存储在磁盘或者等三方中间件redis中。所以它们肯定是有一定的空间大小的,那就需要一些缓存淘汰策略。

问题三:在mybatis中,二级缓存是存在一个命中率记录器的。那么每次查询命中二级缓存,也是需要进行记录的。那么在并发情况下,怎么保证二级缓存命中率的准确性

等等,当然还有一些其他需要考虑问题,我就不一 一列举,

如果让我们设计一个获取缓存的方法,需要解决以上问题的话。那还不简单,直接撸一个方法,把这些解决方法逻辑加上去,就完事。但是在mybatis中,并没有采用这种方式,而是采用了责任链和装饰器设计模式。
在这里插入图片描述

在mybatis当中,首先定义了一个Cache接口,Cache接口定义了一些获取缓存,存放缓存等等一些方法。

通过责任链设计模式,让每一个处理逻辑的Cache去实现Cache接口,在每一个方法中做自己做的事。这样做可以达到我们的解耦和单一职责。

通过装饰器设计模式,当我们处理完线程同步的逻辑后,就会执行下一个cache对应的方法,最后一直执行到我们的内存存储的cache。这样做可以达到解耦,对原有方法进行增强,减少我们的继承类。

总之mybatis这样做的好处,就在于对代码进行可维护可拓展。如果后续有需求需要新加一个cache的逻辑,我们就可以不改动原代码,只需去编写新逻辑的cahche在把它加进去即可。

下面我们来验证下,是不是这样。

 Cache cache = sqlSession.getConfiguration().getCache("org.apache.ibatis.mapper.UserMapper");
    cache.putObject("123",users);

大家可以发现第一个cache是SynchronizedCache,第二个cache是LoggingCache,第三个cache是LRUCache,第四个cache是PerptualCache。其实就和我们上面的流程图一致
在这里插入图片描述


二级缓存执行流程

在这里插入图片描述

在之前流程图中,我做了一些改动。多了一个事务缓存管理器和一个缓存空间。

事务缓存管理器是存储我们当前sqlSession中二级缓存所存储的数据。当SqlSession关闭时,那么这个事务缓存管理器也会失效。为什么要有这个事务缓存管理器呢,因为我们的二级缓存是需要在提交后,在进行存储的。如果进行实时提交,事务后面出现回滚等操作。就会出现脏读等问题。


二级缓存获取和commit源码解剖

Debug解析

在没有执行SQL语句之前,TransactionalCacheManager中的transactionalCaches是空,没有任何数据。这是因为我们还没有执行SQL语句,mybatis也不知道需要将那些数据加入到我们的TransactionalCacheManager当中,你查询了哪个缓存mapper,就会将哪个缓存的mapper加入到我们的TransactionalCacheManager当中。
在这里插入图片描述
在我们执行一次查询后,看看有什么变化
在这里插入图片描述
当我们commit之后,会有什么变化
在这里插入图片描述


命中二级缓存源码解剖

CachingExecutor 的query方法

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
     // 1.获取当前mappernamespace是否配置了<cache/>标签,是否开启了二级缓存 ,从二级缓存空间中获取对应的cache
    Cache cache = ms.getCache();
    if (cache != null) {
    	// 判断是否要清除缓存,只会清空当前线程的缓存,不会影响其他线程的缓存。
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
       // 检查SQL语句中是否包含输出参数
        ensureNoOutParams(ms, boundSql);
        // 从二级缓存中获取结果
        List<E> list = (List<E>) tcm.getObject(cache, key);
        
        // 从一级缓存中获取结果并将结果存储到二级缓存map当中
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        //  从二级缓存中拿出结果,进行返回
        return list;
      }
    }
    // delegate是我们的一级缓存,如果没有开启二级缓存,就直接调用一级缓存获取结果进行返回
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

我们进入tcm也就是TransactionalCacheManager的getobject方法

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
// 获取一个TransactionalCache对象,并将其存储在缓存中,以便在事务中使用。如果缓存中已经存在这个对象,则直接返回;否则,创建一个新的对象并存储在缓存中。
  private TransactionalCache getTransactionalCache(Cache cache) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }
 public Object getObject(Object key) {
    // 从二级缓存空间获取数据
    Object object = delegate.getObject(key);
   // 如果没有获取到
    if (object == null) {
    // 将当前数据加到事务缓存管理器中,进行暂时存储
      entriesMissedInCache.add(key);
    }
    // 判断该值是否为true。如果是true就返回null。
    // 因为在我们事务没提交时,如果进行修改的时候不会直接清除二级缓存空间,而是先标记一下做个假删除。等commit之后才会去清除我们的二级缓存空间。
    // 同样标记为null,也是为了防止出现脏读等。
    if (clearOnCommit) {
      return null;
    }
    return object;
  }

在这里插入图片描述


commit源码解剖

DefaultSqlSession的commit

  public void commit(boolean force) {
    try {
    // 调用二级缓存的commit
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

CachingExecutor的commit
在这里插入图片描述
TransactionalCacheManager的commit

  public void commit() {
  // 循环进行提交
    for (TransactionalCache txCache : transactionalCaches.values()) {
    // 调用TransactionalCache的commit
      txCache.commit();
    }
  }

TransactionalCache的commit

  public void commit() {
    // 如果标记假删除,这里提交时候就会清空
    if (clearOnCommit) {
      delegate.clear();
    }
    // 进行刷新
    flushPendingEntries();
    reset();
  }

// 看到这里是不是就是很熟悉了,将我们事务缓存管理器的map丢到我们的二级缓存空间当中。
  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

至于二级缓存失效,第一个二级缓存只有提交后才会生效,第二个发生增删改就会清空所有的二级缓存。


总结

如上就是对myabtis的一级缓存二级缓存进行分析和源码解剖,如下就做个总结。

1.mybatis一级缓存和二级缓存的区别

1.作用范围不同:一级缓存是SqlSession级别的缓存,只在当前SqlSession中有效;而二级缓存是Mapper级别的缓存,多个SqlSession可以共享同一个Mapper的二级缓存。

2.存储位置不同:一级缓存是存储在SqlSession内部的一个HashMap中,而二级缓存是存储在外部的缓存中,例如Ehcache、Redis等。

3.失效机制不同:一级缓存的失效机制是基于SqlSession的生命周期,当SqlSession关闭时,一级缓存也会被清空;而二级缓存的失效机制是基于时间的,可以通过配置缓存的过期时间来控制缓存的失效。

4.数据一致性不同:一级缓存可以保证数据的一致性,因为它只在当前SqlSession中有效;而二级缓存可能会出现数据不一致的问题,因为多个SqlSession可以共享同一个Mapper的二级缓存。

5.配置方式不同:一级缓存是默认开启的,无需进行额外的配置;而二级缓存需要进行额外的配置,包括缓存的类型、大小、过期时间等。

总之,MyBatis的一级缓存和二级缓存都是用于提高查询性能的缓存机制,但是它们的作用范围、存储位置、失效机制、数据一致性和配置方式都有所不同。在使用MyBatis时,需要根据具体的业务需求和系统性能要求,选择合适的缓存机制。


2.一级缓存失效原因有哪些

  1. 必须在同一个statementID的语句中执行
  2. SQL语句和参数值必须一致。
  3. 分页条件必须一致
  4. 必须在同一个会话中,在上面代码中我们也可以看到。我们调用mapper.selectUser(user);是会执行sqlSession的excutor方法。如果sqlSession关闭了,那么里面的一级执行器,当然也就不会在存在了。

3.为什么不建议使用mybatis的二级缓存?

虽然MyBatis的二级缓存可以提高查询性能,但是在实际应用中,使用二级缓存并不总是一个好的选择。以下是一些原因:

1.数据不一致性问题:二级缓存是一个全局的缓存,多个SqlSession共享同一个缓存。如果在一个SqlSession中修改了数据,但是这个修改没有及时更新缓存,那么在另一个SqlSession中查询这个数据时,就会出现数据不一致的问题。

2.内存占用问题:二级缓存是一个全局的缓存,会占用大量的内存。如果缓存中存储了大量的数据,那么就会导致内存占用过高,从而影响系统的性能。

3.缓存失效问题:二级缓存的失效机制是基于时间的,如果缓存中的数据长时间没有被访问,那么就会被自动清除。但是这种失效机制并不总是可靠的,有时候缓存中的数据可能已经过期,但是仍然被使用,从而导致数据不一致的问题。

4.配置复杂性问题:使用二级缓存需要进行复杂的配置,包括缓存的类型、大小、过期时间等。如果配置不当,就会导致缓存的效果不佳,甚至影响系统的性能。

5.综上所述,虽然MyBatis的二级缓存可以提高查询性能,但是在实际应用中,使用二级缓存需要考虑到数据一致性、内存占用、缓存失效和配置复杂性等问题。因此,建议在使用MyBatis时,根据具体的业务需求和系统性能要求,谨慎选择是否使用二级缓存。

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

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

相关文章

如何把ipa文件(iOS安装包)安装到iPhone手机上? 附方法汇总

苹果APP安装包ipa如何安装在手机上&#xff1f;很多人不知道怎么把ipa文件安装到手机上&#xff0c;这里就整理了苹果APP安装到iOS设备上的方式&#xff0c;仅供参考 苹果APP安装包ipa如何安装在手机上&#xff1f;使用过苹果手机的人应该深有感触&#xff0c;那就是苹果APP安…

opencv 裁剪超大 tif 图像制作深度学习数据集

文章目录 1. 尝试暴力裁剪&#xff08;24 位&#xff09;2. 带透明度读取 &#xff08;32 位&#xff09;3. 裁剪标签&#xff08;8 位&#xff09; 本文只是进行了简单的尝试&#xff0c;未单独处理最右侧、最下侧和右下角区域&#xff0c;且未设置重叠率 若使用 gdal 进行 ti…

【1】Numpy基础

1 N维数组对象&#xff08;ndarray&#xff09; 先理解如何通过numpy进行批次计算&#xff1a; 引入numpy&#xff0c;随机生成数组&#xff1a; import numpy as npdatanp.random.randn(2,3) print(data) print(data*10) print(datadata)[[ 0.88001557 -0.70682337 0.81193…

Linux网络编程——基于UDP协议的简易聊天室

0.关注博主有更多知识 操作系统入门知识合集 目录 1.UDP服务端 1.1消息转发的实现 2.UDP客户端 3.效果展示 1.UDP服务端 使用C、C混编的方式在Linux环境下实现一个简单的UDP服务端。那么我们先看代码&#xff0c;然后逐步分析&#xff1a; // udpServer.hpp #pragma o…

sql 性能优化基于explain调优(二)

文章目录 Explain问题描述解决方案 Explain 关于Explain具体怎么用以及有哪些优点&#xff0c;我就不过多的跟大家去讲解了&#xff0c;从我最初的文章: explain是什么&#xff1f;explain优缺点及如何使用explain优化SQL&#xff0c;大家可以点击这个链接看一下&#xff0c;对…

Midjourney基础教程

本教程收集于:AIGC从入门到精通教程 Midjourney基础教程 目录 新手快速入门知识汇总:

Facebook 广告效果越来越差,怎么办?

在如今的数字营销领域中&#xff0c;Facebook作为独立站卖家首选的推广引流平台&#xff0c;具备了许多优势。 一方面&#xff0c;Facebook拥有庞大的用户数量&#xff0c;是全球最大的社交媒体平台之一。另一方面&#xff0c;Facebook的广告算法可以将广告推送给更加精准的受…

创建.Net MAUI工程

下载Visual Studio或Visual Studio for Mac或Rider&#xff08;收费&#xff09; Visual Studio for Mac 2022Visual StudioRider 工具推荐 如果使用的是Windows电脑&#xff0c;建议使用Visual Studio 2022并安装ReSharper插件。如果使用的是Mac电脑&#xff0c;建议使用Ri…

【教程】用GraphSAGE和UnsupervisedSampler进行节点表示学习

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 目录 无监督的GraphSAGE 加载 CORA 网络数据 按需采样的无监督GraphSAGE 无监督取样器&#xff08;UnsupervisedSampler&#xff09; 提取节点嵌入 节点嵌入的可视化 下游任务 数据拆分 分类器训练 无监…

8.1.0版本ELK搭建,不开启xpack认证机制

8.1.0版本ELK搭建&#xff0c;不开启xpack认证机制 部署环境安排下载安装包服务器环境配置部署elasticsearch部署kibana部署logstash部署httpd&#xff0c;filebeat配置kibana页面 部署环境安排 ip部署服务192.168.0.121kibana,elasticsearch192.168.0.83elasticsearch,logsta…

Spring boot 注解@Async不生效 无效 不起作用

今天在做公司项目时&#xff0c;有一个发邮件的需求。所以写了一个发送邮件的方法后来发现发邮件很慢&#xff0c;导致接口响应也很慢。于是我便想到要使用异步调用去处理这个方法。于是我把注解Async 加到了自己service类下的一个发邮件的一个方法&#xff0c;后来发现并没有生…

Push rejected,用Git修改已提交的注释

问题&#xff1a;有时候因注释与git规定的模板不匹配&#xff0c;会导致远程提交被拒绝 Push rejected 解决&#xff1a;修改不符合规范的注释再push即可 1、打开命令窗口 在项目根目录下右键点击出 Git批处理命令窗口。 2、查看已提交的commit 运行命令&#xff1a;git reba…

dataease源码阅读

源码&#xff1a;https://gitee.com/fit2cloud-feizhiyun/DataEase.git 文件夹目录 1.仪表盘主路由&#xff1a;frontend/src/views/panel |-- penel |-- index.vue |-- panel.js |-- appTemplate | |-- AppTemplateContent.vue | |-- index.vue | |-- component | |-- AppT…

华为OD机试真题 Java 实现【机器人活动区域】【2023Q1 200分】

一、题目描述 现有一个机器人&#xff0c;可放置于 M N的网格中任意位置&#xff0c;每个网格包含一个非负整数编号。当相邻网格的数字编号差值的绝对值小于等于 1 时&#xff0c;机器人可在网格间移动 问题&#xff1a;求机器人可活动的最大范围对应的网格点数目。 说明&a…

ESP32-C2开发板Homekit例程

准备 1.1硬件ESP32 C2开发板&#xff0c;如图1-1所示 图1-1 ESP32 C2开发板 1.2软件 CozyLife APP可以在各大应用市场搜索下载&#xff0c;也可以扫描二维码下载如图1-2所示 HomeKit flash download tool 烧录工具 esp32c2 homkit演示固件 烧录教程 打开flash_download_to…

每日一题161——对角线遍历

给你一个大小为 m x n 的矩阵 mat &#xff0c;请以对角线遍历的顺序&#xff0c;用一个数组返回这个矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;mat [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,4,7,5,3,6,8,9] 示例 2&#xff1a; 输入&#xff1a;mat …

【大数据学习篇8】 热门品类Top10分析

在HBase命令行工具中执行“list”命令&#xff0c;查看HBase数据库中的所有数据表。学习目标/Target 掌握热门品类Top10分析实现思路 掌握如何创建Spark连接并读取数据集 掌握利用Spark获取业务数据 掌握利用Spark统计品类的行为类型 掌握利用Spark过滤品类的行为类型 掌握利用…

【嵌入式烧录刷写文件】-1.4-移动Motorola S-record(S19/SREC/mot/SX)中指定地址范围内的数据

案例背景&#xff08;共5页精讲&#xff09;&#xff1a; 有如下一段S19文件&#xff0c;将源地址范围0x9100-0x9104中数据&#xff0c;移动至一个“空的&#xff0c;未填充的”目标地址范围0xA000-0xA004。 S0110000486578766965772056312E30352EA6 S123910058595A5B5C5D5E5…

调用返回风格

主程序子程序 面向过程 单线程控制&#xff0c;把问题划分为若干个处理步骤&#xff0c;构件即为主程序和子程序&#xff0c;子程序通常可合成为模块。过程调用作为交互机制&#xff0c;即充当连接件的角色。调用关系具有层次性&#xff0c;其语义逻辑表现为主程序的正确性取…

nodejs微信小程序 vue+uniapp停车场车位管理系统sringboot+python

使用微信小程序进行应用开发&#xff0c;使用My SQL软件搭建数据库&#xff0c;管理后台数据并使用Java语言进行程序设计&#xff0c;借鉴国内现有的停车场管理系统&#xff0c;在他们的基础上进行增减和创新&#xff0c;使用Photoshop完成升降式停车场管理系统的界面部件设计&…