MyBatis是纸老虎吗?(七)

news2024/11/23 12:05:14

在上篇文章中,我们对照手动编写jdbc的开发流程,对MyBatis进行了梳理。通过这次梳理我们发现了一些之前文章中从未见过的新知识,譬如BoundSql等。本节我想继续MyBatis这个主题,并探索一下MyBatis中的缓存机制。在正式开始梳理前,个人觉得弄清楚缓存是什么,为什么要有缓存是很重要的。那何为缓存?相信配置过计算机的诸位都知道,CPU中有一个名为缓存的部件,它的作用就是缓解高速运转CPU和低速运转磁盘之间数据处理速度不协调的问题。由此,我们可以给缓存一个这样的定义:所谓缓存就是具有缓冲作用的事物,这里的缓冲,即缓解,也就是让矛盾双方之间的矛盾弱化,甚至消失。譬如:在政治和军事上起到缓冲作用的朝鲜和韩国,在现代汽车中起到缓冲作用的安全气囊,在计算机上起到缓冲作用的内存以及CPS一二级缓存等等。如果这个定义可用,那为什么要用缓存这个问题就迎刃而解了:为了缓解矛盾双方的矛盾。按照这个思路去理解MyBatis中的缓存或许就不是什么难事了。

1 MyBatis缓存

在java知识体系中,MyBatis是一个非常常见的数据库访问组件,其可以帮开发者减少开发jdbc代码的烦恼,同时提供一些高效率的数据转换组件以减少开发者编写数据转换代码的痛苦。不过MyBatis不仅仅有这些好处,它还提供了更多实用的功能,譬如本篇文章要梳理的缓存。在MyBatis中引入缓存的主要目的就是缓解矛盾双方之间的矛盾,这里矛盾的双方,从微观角度看是程序和数据库,从宏观角度看是用户和互联网企业。如果刨除其他噪音,我们会发现用户和互联网企业之间的矛盾是程序和数据库之间的矛盾的延申,因为互联网公司是通过对外提供的APP或应用来为用户服务的,说白了用户购买的是互联网公司的服务。如果用户体验不好,那互联网公司就无法长久立足市场!不会吧,一个MyBatis缓存就能引发这么大的变故?这个暂且不说,根据上面的逻辑,MyBatis缓存是为了减少数据库压力,提高数据库性能而出现的。那MyBatis是如何实现这个缓存功能的呢?网络有位大神是这么讲的(原文:MyBatis缓存看这一篇就够了(一级缓存+二级缓存+缓存失效+缓存配置+工作模式+测试)):MyBatis缓存的原理是这样的,数据从数据库中查出来以后,被包装为相应对象,这个对象在使用完后不会被立即销毁,而是储存在MyBatis提供的一个缓冲区中,当再次使用时,直接从缓冲区把这个数据拿出来即可。这样直接从内存中获取数据,从而不再向数据库发送select执行命令的过程,可以减少数据库查询次数,从而提高了数据库的性能。在MyBatis中这个缓冲区(又可以被称为缓存)是通过Map集合实现的

在MyBatis中,缓存可以分为两类:一级缓存和二级缓存。其中一级缓存的作用域是同一个SqlSession,在同一个SqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库查询的数据写到缓存(内存),第二次会从缓存中获取数据而不进行数据库查询,可以大大提高了数据查询效率。当一个SqlSession结束后该SqlSession中的一级缓存也就不复存在了。MyBtais默认是启动一级缓存的二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递的参数也相同时,第一次执行完毕会将数据库中查询到的数据写到缓存(内存),第二次会直接从缓存中获取,从而提高了查询效率。MyBatis默认不开启二级缓存,需要在MyBtais全局配置文件中进行setting配置开启二级缓存

2 MyBatis一级缓存

通过第一小节的概念性描述我们认识了MyBatis中的缓存,知道了在MyBatis中存在两种类别的缓存:一级缓存(一级缓存的生命周期和SqlSession是一个级别)和二级缓存(二级缓存的生命周期和Mapper是一个级别)。其中一级缓存默认是开启的(在MyBatis的配置文件中也可以添加一级缓存开启的配置,具体代码为:<setting name="localCacheScope" value="SESSION"/>),二级缓存需要配置才能开启(具体代码为:<setting name="cacheEnabled" value="true"/>)。通过代码跟踪我们也发现当同时执行两条相同的sql语句时,在第二次执行时,MyBatis会直接从缓存中提取数据。那MyBatis究竟是怎么实现一级缓存的呢?先看一下本节案例执行入口的代码,如下图所示:

其实这段代码和《MyBatis是纸老虎吗?(一)》这篇文章中的案例二基本类似,只不过最后两行代码不太一样。接下来开始执行这个案例,首先程序会在图中第一个断点处停下,之后进入DefaultSqlSession.selectList()方法即可。然后继续,进入DefaultSqlSession中接收四个参数的selectList()方法中,其参数为:String、Object、 RowBounds、ResultHandler。该方法的源码如下所示:

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    dirty |= ms.isDirtySelect();
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

这段代码很简单,唯一需要关注的就是Executor.query()这行代码。由于跟踪时,executor的实际类型为CachingExecutor,所以下面要看的方法是CachingExecutor中的query()方法,这段代码的运行时状态,如下图所示:

这段逻辑中,我们重点看createCacheKey()这行代码,从调用不难发现,这个被调用的方法位于CachingExecutor类中,源码为:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}

由于目前正在被使用的CachingExecutor对象所持有的delegate属性的实际类型为SimpleExecutor,而这个类又继承了BaseExecutor(注意这个类又实现了Executor接口)抽象类。由于SimpleExecutor类中只有doUpdate()、doQuery()、doQueryCursor()、doFlushStatements()及prepareStatement()这几个方法,所以上述源码中的createCacheKey()方法位于BaseExecutor类中,这个方法的源码如下所示:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  cacheKey.update(ms.getId());
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  cacheKey.update(boundSql.getSql());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // mimic DefaultParameterHandler logic
  MetaObject metaObject = null;
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        if (metaObject == null) {
          metaObject = configuration.newMetaObject(parameterObject);
        }
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

这段代码的处理逻辑非常清晰,判断当前对象上的close属性是否为true,如果是就抛出异常,否则就创建CacheKey对象,并调用该对象上的update()方法。为了更加深入的了解这段代码的处理逻辑,我们来看一下CacheKey类的源码,如下所示:

public class CacheKey implements Cloneable, Serializable {

  private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new CacheKey() {

    private static final long serialVersionUID = 1L;

    @Override
    public void update(Object object) {
      throw new CacheException("Not allowed to update a null cache key instance.");
    }

    @Override
    public void updateAll(Object[] objects) {
      throw new CacheException("Not allowed to update a null cache key instance.");
    }
  };

  private static final int DEFAULT_MULTIPLIER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  // 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this
  // is not always true and thus should not be marked transient.
  private List<Object> updateList;

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public int getUpdateCount() {
    return updateList.size();
  }

  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if ((hashcode != cacheKey.hashcode) || (checksum != cacheKey.checksum) || (count != cacheKey.count)) {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

  @Override
  public int hashCode() {
    return hashcode;
  }

  @Override
  public String toString() {
    StringJoiner returnValue = new StringJoiner(":");
    returnValue.add(String.valueOf(hashcode));
    returnValue.add(String.valueOf(checksum));
    updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
    return returnValue.toString();
  }

  @Override
  public CacheKey clone() throws CloneNotSupportedException {
    CacheKey clonedCacheKey = (CacheKey) super.clone();
    clonedCacheKey.updateList = new ArrayList<>(updateList);
    return clonedCacheKey;
  }

}

从源码可以看出这个类非常简单,拥有五个属性,它们分别为:int类型的multiplier,int类型的hashcode,long类型的checksum,int类型的count以及List<Object>类型的updateList。这个类拥有两个构造方法,一个无参构造方法,一个接收Object[]数组的有参构造方法。其中无参构造方法主要是给这个类的其中四个属性赋值,它们分别是:updateList(默认数据是空的ArrayList对象)、hashcode(默认值是17)、multiplier(默认值是37)、count(默认值是0)。有参构造方法会首先调用无参构造方法,然后再调用updatAll()方法。这个updateAll()方法的处理逻辑非常简单,就是遍历数据,然后分别调用update()方法。这个update()方法的处理逻辑很简单,就是计算传递进来的Object对象的hash值,然后做一些列运算,得出一个数据赋值给CacheKey的hashcode属性,最后传进本方法的object数据存到updateList数组对象中。这个类还有一个getUpdateCount(),其作用就是返回updateList数组中的数据量。还有这个类重写了Object类中的equals()方法和toString()方法,我们知道equals()方法是两个对象比较时常用的方法,所以重写这个方法的目的是重写这两个对象的比对逻辑,而重写toString()方法是为了以约定的方式向调用者展示自身数据。由于这个类实现了Cloneable接口,所以这个类又重写了Cloneable中的clone()方法。总体来说,CacheKey类是MyBatis框架提供的一个缓存key,目的是在我们缓存sql查询结果时使用。现在再回到BaseExecutor类的createCacheKey()方法中,此时再看这一系列的cacheKey.update()方法,我们似乎不再那么茫然了,这一系列操作的目的不过就是让CacheKey的hashcode值唯一。参与hashcode值计算的数据有:sql语句的id值、查询语句的分页偏移量、查询语句的分页条数、BoundSql中的sql属性、parameterMapping(如果有的话)、environmentId(如果有的话)等。待CacheKey创建完成后,持续向上返回,直到返回到CachingExecutor中的query()方法为止。此时可以看到该方法的最后一行代码,如下图所示:

可以发现这段代码调用了CachingExecutor类中接收六个参数的query()方法,这些参数分别为:MappedStatement、Object、RowBounds、ResultHandler、CacheKey、BoundSql,具体如下图所示:

这个方法在整个系列中被看了很多次,为了完整理解,这里再贴一下这个方法的源码,如下所示:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
    CacheKey key, BoundSql boundSql) throws SQLException {
  Cache cache = ms.getCache();
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

从源码可以看出,这段代码会首先从MappedStatement中拿一下Cache,如果缓存不为空则走if分支,否则继续调用SimpleExecutor类中的query()方法。我们跟踪的案例没有走if分支,所以这里直接看SimpleExecutor中的query()方法(从BaseExecutor类继承来的方法)的执行状态如下图所示:

先看图中list = resultHandler == null ? (List<E>) localCache.getObject(key) : null这段代码,这里会先判断resultHandler是否null,如果时,则直接执行localCache.getObject(key)这段代码,否则直接返回null。由于调用该方法时传递进来的ResultHandler对象为null,所以这里直接执行localCache.getObject(key)方法,最终返回了null,即承载数据库查询结果的List<T>对象的结果为null,接下了会直接执行BaseExecutor类中的queryFromDatabase()方法,执行情况如下图所示:

由图可以看出,这个方法首先会向localCache对象中放一个key(前面执行CachingExecutor中的query()方法时创建的CacheKey对象),其对应的数据为EXECUTION_PLACEHOLDER枚举。接下来该方法会直接调用BaseExecutor子类实现的doQuery()方法,这里的子类是SimpleExecutor类。紧接着finally块中会把localCache对象中的CacheKey移除。接着再向localCache对象中的存放CacheKey及其对应的数据doQuery()方法返回的结果。然后向上返回doQuery()方法的查询结果。直至最初调用者,即SpringTransactionApplication类的mybatis()方法中。

紧接着程序会继续调用同样的查询语句进行查询。下面用一章图片展示一下程序的运行时状态,具体如下图所示:

由于前面已经梳理过执行流程,所以这次会跳过非关键环节,直接看BaseExecutor类的query()方法的执行状态,如下图所示:

最终会执行list = resultHandler == null ? (List<E>) localCache.getObject(key) : null,跟前面执行逻辑一样。由于resultHandler是null,这里会直接执行localCache.getObject(key)这个逻辑,由于这里localCache中存在数据,具体如下图所示:

根据上面这张图片可以直到最终list对象不为空,会有二十五条数据,具体可以看下面这幅图片:

最终代码走到了handleLocallyCacheOutputParameters()方法中,这个方法位于BaseExecutor类中,这个方法的源码如下所示:

private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter,
    BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    final Object cachedParameter = localOutputParameterCache.getObject(key);
    if (cachedParameter != null && parameter != null) {
      final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
      final MetaObject metaParameter = configuration.newMetaObject(parameter);
      for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
        if (parameterMapping.getMode() != ParameterMode.IN) {
          final String parameterName = parameterMapping.getProperty();
          final Object cachedValue = metaCachedParameter.getValue(parameterName);
          metaParameter.setValue(parameterName, cachedValue);
        }
      }
    }
  }
}

通过上面的梳理,我们可以看到MyBatis的一级缓存对象在Executor的实现类BaseExecutor中,其名字为localCache,该变量的类型为:PerpetualCache,并且查询缓存的时间点就在将要去查询数据库的时候,从代码层面看,这个逻辑位于BaseExecutor类的queryFromDatabase()方法中。下面让我们一起看一下PerpetualCache的源码吧:

public class PerpetualCache implements Cache {

  private final String id;
  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }
  @Override
  public int getSize() {
    return cache.size();
  }
  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }
  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }
  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }
  @Override
  public void clear() {
    cache.clear();
  }
  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }
  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();
  /**
   * @param key
   *          Can be any object but usually it is a {@link CacheKey}
   * @param value
   *          The result of a select.
   */
  void putObject(Object key, Object value);
  /**
   * @param key
   *          The key
   *
   * @return The object stored in the cache.
   */
  Object getObject(Object key);
  /**
   * As of 3.3.0 this method is only called during a rollback for any previous value that was missing in the cache. This
   * lets any blocking cache to release the lock that may have previously put on the key. A blocking cache puts a lock
   * when a value is null and releases it when the value is back again. This way other threads will wait for the value
   * to be available instead of hitting the database.
   *
   * @param key
   *          The key
   *
   * @return Not used
   */
  Object removeObject(Object key);
  /**
   * Clears this cache instance.
   */
  void clear();
  /**
   * Optional. This method is not called by the core.
   *
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();
  /**
   * Optional. As of 3.2.6 this method is no longer called by the core.
   * <p>
   * Any locking needed by the cache must be provided internally by the cache provider.
   *
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

从代码可以看出PerpetualCache类实现了Cache接口。Cache接口提供了一系列方法,这些方法有:

  1. String getId():获取当前Cache对象的id值
  2. void putObject(Object key, Object value) :向当前Cache对象中存放指定key,及其对应的数据
  3. Object getObject(Object key) :根据key,从当前Cache对象中提取数据
  4. Object removeObject(Object key) :从当前Cache对象中移除指定key的数据
  5. void clear():清空当前Cache对象中的数据
  6. int getSize():查询当前Cache对象中的数据总量

由于PerpetualCache实现了Cache接口,因此会对这些接口方法进行实现。同时因为PerpetualCache是通过Map<String, Object>类型的属性cache来缓存从数据库中查询出来的数据集合,所以这些实现方法的操作逻辑,本质上就是对Map的操作。梳理到这里我们还是一起回顾一下Executor的继承结构(注意Executor的实现类BaseExecutor中有一个PerpetualCache类型的localCache属性用于缓存数据),具体如下图所示:

3 MyBatis二级缓存

在前一小节开头我们提到了在MyBatis中启用二级缓存的方法。那这个二级缓存究竟是怎么实现的?下面就让我们一起看看吧!

未完待梳理,敬请谅解

4 总结

在总结前,我想对通过网络搜索到的两篇优质博文的创作者说声谢谢,这两篇博文分别是:《MyBatis缓存看这一篇就够了(一级缓存+二级缓存+缓存失效+缓存配置+工作模式+测试)》和《聊聊MyBatis缓存机制》。由于他们出色得整理,我仅用了半天时间就搞懂了MyBatis缓存。下面就本篇梳理的知识点做一下总结:

首先通过梳理,我对缓存有了更加深刻的认识,并且知道了MyBatis添加缓存功能的原因:缓解矛盾双方的对立。这里对立的双方从宏观角度讲是用户和公司,从微观角度看是程序和数据库。

接着通过梳理,我对MyBatis一级缓存有了更加深刻的了解,并且通过梳理我知道了MyBatis中启用一级缓存的方式,一级缓存的实现原理及一级缓存的工作流程。在MyBatis中一级缓存的生命周期和SqlSession一致;一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺;一级缓存最大范围是SqlSession内部,如果有多个SqlSession或者在分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement

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

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

相关文章

Python自动化测试环境搭建

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号&#xff1a;互联网杂货铺&#xff0c;回复1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 请事先自行安装好​​Pycharm​​​软件哦&#xff0c;我…

【Python系列】获取字符串的长度

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Clip Converter - 视频在线下载方法

Clip Converter - 视频在线下载方法 1. Video URL to Download2. Continue3. StartReferences YT to MP4 & MP3 Converter! https://www.clipconverter.cc/ Clip Converter is a free online media conversion application, which allows you to reocord, convert and do…

基于冠豪猪优化器(CPO)的无人机路径规划

该优化算法是2024年新发表的一篇SCI一区top论文具有良好的实际应用和改进意义。一键运行main函数代码自动保存高质量图片 1、冠豪猪优化器 摘要&#xff1a;受冠豪猪(crest Porcupine, CP)的各种防御行为启发&#xff0c;提出了一种新的基于自然启发的元启发式算法——冠豪猪…

#Linux系统编程(ps和kill命令)

&#xff08;一&#xff09;发行版&#xff1a;Ubuntu16.04.7 &#xff08;二&#xff09;记录&#xff1a; &#xff08;1&#xff09;ps命令 可以列出系统中当前运行的那些进程。 命令格式&#xff1a;ps 参数(常用-aux) 命令功能&#xff1a;用来显示当前进程的状态 常…

应急响应实战笔记04Windows实战篇(2)

第2篇&#xff1a;蠕虫病毒 0x00 前言 ​ 蠕虫病毒是一种十分古老的计算机病毒&#xff0c;它是一种自包含的程序&#xff08;或是一套程序&#xff09;&#xff0c;通常通过网络途径传播&#xff0c;每入侵到一台新的计算机&#xff0c;它就在这台计算机上复制自己&#xff…

Star 5.3k!纯Python开发的高效SQL 解析器!

目前从事大数据相关的开发&#xff0c;都离不开SQL&#xff0c;不管是关系型数据库还是非关系型数据&#xff0c;在做不同数据库间迁移或者转换的时候都会用到SQL转换。今天来为大家分享一个有趣的开源项目&#xff0c;SQLGlot&#xff0c;一个纯Python开发的SQL转换器&#xf…

VMware扩容硬盘

最近研究Oracle的备份导入导出功能&#xff0c;但是因为磁盘容量不够导致表空间的扩容没办法&#xff0c;从而没办法导入数据库的dmp文件。得想办法先扩容磁盘容量。话不多说上截图操作。 操作环境&#xff1a;VMware10 , Centos 6.9 VMware扩容硬盘步骤 一、关闭虚拟机&…

sizeof和strlen的使用及差异

sizeof 1.sizeof是操作符 2.sizeof计算操作数所占内存的大小&#xff0c;单位是字节&#xff08;byte&#xff09; 3.sizeof不关注内存中存放什么数据 4.sizeof比较通用不挑类型 strlen 1.strlen是库函数&#xff0c;使用需包含头文件string.h 2.strlen是求字符串长度的&#x…

L1-2 倒数第N个字符串

本题链接&#xff1a;PTA | 程序设计类实验辅助教学平台 题目&#xff1a; 样例&#xff1a; 输入 3 7417 输出 pat 思路&#xff1a; 根据题意&#xff0c;这道题是一道思维模拟题。 给出 n 位的小写字母字符串&#xff0c;其中进制位为 26 &#xff0c;求进位1 到 倒数第 …

天工AI搜索引擎

相信正在看autosar架构相关内容的人来说&#xff0c;对于autosar相关知识或者配置项的生涩知识点可谓是苦之久矣&#xff0c;这个时候一个好的搜索引擎能带来的帮助太大了&#xff0c;不管是平时百度还是看文档都需要大量的时间去检索自己真正想知道的信息&#xff0c;偶然间发…

酒店管理系统【GUI/Swing+MySQL】(Java课设)

系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设&#xff01;&#xff01;&#xff01; 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址&#xff1a;https://download.csdn.net/download/qq_50954361/89036287 更多系统资源库…

Windows 频繁失去焦点分析

原文&#xff1a;https://blog.iyatt.com/?p14383 1 前言 刚才在打字的时候发现会随机失去焦点&#xff0c;然后又要用鼠标点一下正在输入的位置才能继续输入&#xff0c;特别烦。开始我怀疑是手碰到触摸板导致失去焦点&#xff0c;但是我用了差不多十年带触摸板的笔记本电脑…

Python框架篇(7):FastApi-依赖项

有时选择太多也会让人陷入焦虑&#xff0c;比如突然有一段自由时间&#xff0c;却因为想做的事情太多&#xff0c;最后把时间都浪费在了摇摆不定上&#xff0c;静不下心做最重要的事&#xff0c;或者说根本不知道最重要的事情是什么。---------- 《认知觉醒:开启自我改变的原动…

【干货】无源滤波器设计讲解,工作原理+设计步骤

今天给大家分享的是&#xff1a;无源模拟滤波器针对很多入门小白不懂滤波器设计&#xff0c;一些老工程师上班很多年有的也不懂得总结知识点&#xff0c;以及想学习不知道怎么系统学习的这一类人群&#xff0c;前方知识点来袭&#xff0c;请君放心食用~ 在信号处理领域&#x…

AXI-Stream——草稿版

参考自哔站&#xff1a;FPGA IP之AXI4-Lite AXI4-Stream_哔哩哔哩_bilibili 信号 传输层级从小到大 TKEEP和TSTRB共同决定了是哪种数据流

红外循迹,避障模块介绍

本节将介绍标题中三种模块的应用以及工作原理 上图中&#xff0c;黑色的是红外接收管&#xff0c;绿色的是红外发射管&#xff0c;他俩与发光二级管都非常像&#xff0c;但功能上却有所不同。 发光二级管&#xff1a;亮度在一定的时间内随电流的增大而增大。 红外发射管&…

Python遥感开发之解决TIF数据之间行列不一致的问题

Python遥感开发之解决TIF数据之间行列不一致的问题 1.问题如图所示2.完整代码如下所示 前言&#xff1a;主要解决在同一分辨率的情况下&#xff0c;遥感数据之间行和列数据不一致的问题。 1.问题如图所示 我们发现这两个TIF的分辨率是一样的&#xff0c;都是0.01x0.01&#xff…

软考软件设计师2024年5月报名流程及注意事项

2024年5月软考软件设计师报名入口&#xff1a; 中国计算机技术职业资格网&#xff08;http://www.ruankao.org.cn/&#xff09; 2024年软考报名时间暂未公布&#xff0c;考试时间上半年为5月25日到28日&#xff0c;下半年考试时间为11月9日到12日。不想错过考试最新消息的考友…

Django(三)-搭建第一个应用(2)

一、编写更多视图 问题详情页——展示某个投票的问题和不带结果的选项列表。问题结果页——展示某个投票的结果。投票处理器——用于响应用户为某个问题的特定选项投票的操作。 # 1.问题详情页&#xff1a;展示某个投票的问题和不带结果的选项列表 def detail(request,questi…