MyBatis是纸老虎吗?(七)

在上篇文章中,我们对照手动编写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 logicMetaObject 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 #176cacheKey.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;@Overridepublic void update(Object object) {throw new CacheException("Not allowed to update a null cache key instance.");}@Overridepublic 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);}}@Overridepublic 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;}@Overridepublic int hashCode() {return hashcode;}@Overridepublic 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();}@Overridepublic 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;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic 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());}@Overridepublic 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.rhkb.cn/news/287116.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot+MyBatis框架的智慧生活商城系统的设计与实现(源码+LW+部署+讲解)

目录 前言 需求分析 可行性分析 技术实现 后端框架&#xff1a;Spring Boot 持久层框架&#xff1a;MyBatis 前端框架&#xff1a;Vue.js 数据库&#xff1a;MySQL 功能介绍 前台功能拓展 商品详情单管理 个人中心 秒杀活动 推荐系统 评论与评分系统 后台功能拓…

测试小白必看:自动化测试入门基础知识

一、首先&#xff0c;什么是自动化测试&#xff1f; 自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程。通常&#xff0c;在设计了测试用例并通过评审之后&#xff0c;由测试人员根据测试用例中描述的规程一步步执行测试&#xff0c;得到实际结果与期望结果的比较…

Vuex状态、数据持久化(vue2、vue3状态数据持久化)

简介&#xff1a;Vuex是一个仓库&#xff0c;是vue的状态管理工具&#xff0c;存放公共数据&#xff0c;任何组件都可以使用vuex里的公共数据。Vuex提供了插件系统&#xff0c;允许我们使用 vuex-persistedstate插件&#xff0c;将Vuex的状态持久化到本地存储中&#xff0c;解决…

【工具篇】总结比较几种绘画软件的优缺点

目录 一、Visio二、Processon三、draw.io四、亿图图示五、wps 写在文章开头&#xff0c;感谢你的支持与关注&#xff01;小卓的主页 一、Visio Visio 是微软公司开发的一款流程图和图表绘制软件。我们可以用它来创建各种类型的图表&#xff0c;如流程图、组织结构图、网络图、平…

【python从入门到精通】-- 第二战:注释和有关量的解释

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;python从入门到精通&#xff0c;魔法指针&#xff0c;进阶C&#xff0c;C语言&#xff0c;C语言题集&#xff0c;C语言实现游戏&#x1f448; 希望得到您的订阅和支持~ &#x1f4a1; 坚持创作博文…

java的一些内部小知识,类与对象的关系

目录 1. java2. 类与对象的关系 1. java test.java ---- javac --> Test.class ---- java-----> 内存 ----> cpu 源文件 二进制代码 所有正在运行的软件都在内存中有自己的内存空间 jvm —>运行java程序的&#xff0c;java虚拟机 main(); // 内部调用run()run(i…

Fiddler抓包工具之fiddler的常用快捷键

一、常用三个快捷键 ctrlX :清空所有记录 CtrlF&#xff1a;查找 F12&#xff1a;启动或者停止抓包 使用 QuickExec Fiddler2 成了网页调试必备的工具&#xff0c;抓包看数据。Fiddler2自带命令行控制。 fiddler 命令行快捷键&#xff1a;ctrl q &#xff0c;然后 输入 help…

QTday5

头&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> //定时器事件 #include <QTime> //时间类 #include <QtTextToSpeech> //文本转语音类 #include <QMouseEvent> //鼠标事件类 QT_BEGIN_NAMESPACE …

Redis锁,乐观锁与悲观锁

锁 悲观锁 认为什么时候都会出问题&#xff0c;无论做什么都会加锁 乐观锁 很乐观&#xff0c;认为什么时候都不会出问题&#xff0c;所以不会上锁。 更新数据时去判断一下&#xff0c;在此期间&#xff0c;是否有人修改过这个数据 应用于&#xff1a;秒杀场景 **watch*…

【保姆级讲解Edge兼容性问题解决方法】

&#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d; 希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…

新人应该从哪几个方面掌握大数据测试?

什么是大数据 大数据是指无法在一定时间范围内用传统的计算机技术进行处理的海量数据集。 对于大数据的测试则需要不同的工具、技术、框架来进行处理。 大数据的体量大、多样化和高速处理所涉及的数据生成、存储、检索和分析使得大数据工程师需要掌握极其高的技术功底。 需要你…

【Java程序设计】【C00366】基于(JavaWeb)Springboot的纹理生产图片系统(有论文)

TOC 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;博客中有上百套程序可供参考&#xff0c;欢迎共同交流学习。 项目简介 项目获取 &#x1f345;文末点击卡片…

MySQL 经典练习 50 题 (记录)

前言&#xff1a; 记录一下sql学习&#xff0c;仅供参考基本都对了&#xff0c;不排除有些我做的太快做错了。里面sql不存在任何sql优化操作&#xff0c;只以完成最后输出结果为目的&#xff0c;包含我做题过程和思路最后一行才是结果。 1.过程: 1.1.插入数据 /* SQLyog Ul…

STM32之HAL开发——手动移植HAL库

HAL库移植步骤 创建目录 配置启动文件 在\Drivers\CMSIS\Device\ST\stm32f1xx\Source\Templates\ARM目录下&#xff0c;根据你的芯片型号选择对应的启动文件&#xff0c;不同容量大小的芯片&#xff0c;对应的启动文件也不一样。 注意&#xff1a;在HAL库中&#xff0c;不同容…

centos7二进制安装openstack train版本双网口五节点

这里写目录标题 材料准备宿主机安装KVM 网络规划硬件规划本案例局限性密码规划虚拟机准备网络准备centos7模板机准备 数据库安装安装rabbitMQ消息队列安装memcached服务安装Etcd安装keystone身份服务创建数据库用户keystone安装keystone组件创建admin并启动keystone监听验证key…

canvas画带透明度的直线和涂鸦

提示&#xff1a;canvas画线 文章目录 前言一、带透明度的直线和涂鸦总结 前言 一、带透明度的直线和涂鸦 test.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content…

堆排序(六大排序)

前面博客已经分享过堆的知识了&#xff0c;今天我们来分享堆排序。 堆排序 堆排序(Heapsort)是指利用堆积树&#xff08;堆&#xff09;这种数据结构所设计的一种排序算法&#xff0c;它是选择排序的一种。它是通过堆来进行选择数据。 ★★★需要注意的是排升序要建大堆&#…

【前端Vue】Vue3+Pinia小兔鲜电商项目第3篇:静态结构搭建和分类实现,1. 整体结构创建【附代码文档】

Vue3ElementPlusPinia开发小兔鲜电商项目完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;认识Vue3&#xff0c;使用create-vue搭建Vue3项目1. Vue3组合式API体验,2. Vue3更多的优势,1. 认识create-vue,2. 使用create-vue创建项目,1. setup选项的写法和执行…

苍穹外卖项目笔记

软件开发流程 需求分析&#xff1a;说明书和原型 设计&#xff1a;UI&#xff0c;数据库&#xff0c;接口设计 编码&#xff1a;项目代码&#xff0c;单元测试 测试&#xff1a;测试用例&#xff0c;测试报告 上线运维&#xff1a;软件环境安装&#xff0c;配置 软件环境…

IPhone让用户升级?网友你咋不降点!

最近一个热搜刷屏了我的朋友圈&#xff0c;我点开一看是苹果上架了全新“换代有来”页面&#xff0c;其主要表达了苹果用户可以将旧的iphone升级到全新的iphoe15上。并告诉贴心的给网友对比了一下换代的好处。 并且还详细了说了一些iPhone 11、11 Pro、11 Pro Max、12、12 mini…