手敲Mybatis(16章)-一级缓存功能实现

news2025/1/12 18:18:52

1.实现目的

这一节的目的主要是实现SqlSession级别的缓存,也就是一级缓存,首先看下图一,用户可以通过设置来进行是否开启一级缓存,不设置的化默认开启一级缓存,localCacheScope=SESSION为要设置一级缓存,localCacheScope=STATEMENT为不要设置一级缓存,(所以后面在清理缓存时会进行判断,如果是STATEMENT就删除缓存)。

2.简单说明

然后我们就需要解析下缓存设置,拿到缓存级别,执行Sql语句前,将按Mybatis的规定处理缓存key(id,参数,Sql语句,环境等等,生成对应hash值),然后判断缓存中是否有当前key的结果数据,有的化结果数据直接返回,没有的话就去执行数据库查询,然后将查询的结果存储到一级缓存Map中,然后判断缓存级别是否是STATEMENT,是的话代表不进行缓存操作,那此时删除,下次进来还是继续查询库,不是的话就不删除,留着继续执行同一SqlSession会话使用。

其实不算难,还是很简单,也很有调理,这就是设计的魅力,把每个类与属性现实化,那对应需要改动哪里,就都很清晰。

3.XML图

XML类图就是对应上面所说的逻辑的实现。

1.XMLConfigBuilder里专门解析设置操作,Configuration则是缓存级别的存储,此时需要用到LocalCacheScope枚举类,

2.Executor类里query方法添加一个参数缓存Key参数(CacheKey),然后在BaseExecutor里用没有缓存key参数的quey方法来获取缓存key的操作(这里获取就是生成,将对应需要的参数传到CacheKey类里处理Hash),得到后调用有缓存Key参数的query方法,这时此方法根据当前key去缓存查询是否有数据,有的话缓存拿出,没有执行queryFromDatabase方法,从数据库获取,获取完毕存储缓存里(存储Cache接口PerpetualCache类的map里)

4.代码实现 

4.1 解析缓存设置

XMLConfigBuilder:解析配置XML构建器,这里需要添加私有方法settingsElement,主要解析缓存设置,解析完毕将缓存级别存储到configuration的LocalCacheScope枚举类下。

public class XMLConfigBuilder extends BaseBuilder {

   // 省略其他方法
   
    public Configuration parse() {
       // STEP-17 添加设置
       settingsElement(root.element("settings"));
    }

   /**
     * 解析配置在 XML 文件中的缓存机制。并把解析出来的内容存放到 Configuration 配置项中。
     * <settings>
     * <!--缓存级别:SESSION/STATEMENT-->
     * <setting name="localCacheScope" value="SESSION"/>
     * </settings>
     */
    private void settingsElement(Element context) {
        if (context == null) return;
        List<Element> elements = context.elements();
        Properties props = new Properties();
        for (Element element : elements) {
            props.setProperty(element.attributeValue("name"), element.attributeValue("value"));
        }
        // 设置缓存级别
        configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope")));
    }
}

Configuration:Configuration类里就针对缓存级别进行赋值操作。

public class Configuration {
     // 缓存机制,默认不配置的情况是 SESSION
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

    public LocalCacheScope getLocalCacheScope() {
        return localCacheScope;
    }

    public void setLocalCacheScope(LocalCacheScope localCacheScope) {
        this.localCacheScope = localCacheScope;
    }
}

LocalCacheScope:缓存级别枚举,

/**
 * @Author df
 * @Description: 本地缓存机制;
 * SESSION 默认值,缓存一个会话中执行的所有查询
 * STATEMENT 不支持使用一级缓存,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不做数据共享
 * @Date 2024/1/9 10:10
 */
// step 17添加
public enum LocalCacheScope {
    SESSION,
    STATEMENT
}

4.2 执行器缓存处理

Executor:

1.Executor类的修改是关于query方法时新添加缓存Key(CacheKey)操作,

2.添加了清除一级缓存方法定义以及创建缓存Key的方法定义

然后在BaseExecutor执行query时执行缓存存储,并根据参数生成缓存key,调用createCacheKey方法,增删改时删除缓存操作调用clearLocalCache方法。

public interface Executor {
   // step-17 添加CacheKey参数
   <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException;
   // step-17添加清理Session缓存
   void clearLocalCache();

   //  step-17添加创建缓存 Key
   CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
}

BaseExecutor:

1.update方法:添加了调用此方法就会触发清除缓存操作clearLocalCache();,当然Mybaties的操作增删改全部调用update方法。

2.query定义的有两个方法,所以没有缓存Key的那个需要先生成缓存Key,传给有缓存Key参数的query方法。

3.有缓存Key参数的query方法,先判断queryStack是否为0,是否isFlushCacheRequired强制刷新如果都是则清除下当前缓存,然后根据缓存Key去调用一级缓存(PerpetualCache类)Map里是否存储了数据,没有的话去调用queryFromDatabase方法(查询库拿到数据结果),缓存有的话直接返回就可,最后判断下缓存级别是否STATEMENT,是的话删除缓存,不是的话不操作。

4.queryFromDatabase(),这是添加的私有方法,主要是查询库结果数据,查询到的结果存储到一级缓存中。

5.commit方法的更改就是事务提交就清除缓存,所以是SqlSession级别的吗,rollback方法也是调用此方法就清除缓存。

6.clearLocalCache方法,此方法清除一级缓存。

7.createCacheKey方法, 创建缓存Key的业务处理,按照MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码作为 Key 使用,所以依次把参数传给CacheKey的update的方法。

public abstract class BaseExecutor implements Executor {
    // 省略其他属性,方法    

    // 本地缓存
    protected PerpetualCache localCache;

    private boolean closed;
    // 查询堆栈
    protected int queryStack = 0;

    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.configuration = configuration;
        this.transaction = transaction;
        this.wrapper = this;
        this.localCache = new PerpetualCache("LocalCache");
    }

    // update添加清除缓存操作
    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        // step-17添加-----------------------------start
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        clearLocalCache();
        // step-17添加------------------------------end
        return doUpdate(ms, parameter);
    }

    // query查询时创建缓存Key,将Mybatis规定的参数传入进去。
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        // step-17 创建缓存Key,用这些参数组成缓存使用的Key。
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        // step-17添加
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }

        // 清理局部缓存,查询堆栈为0则清理。queryStack 避免递归调用清理
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            // 根据cacheKey从localCache中拿到结果数据
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list == null) {
                // 缓存没有拿到数据就去数据库查询下
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            // 如果是STATEMENT证明不用缓存,所以此处清理缓存
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                clearLocalCache();
            }
        }
        return list;
        // step-17添加-----------end
    }

     // step-17添加,去数据库查询数据,查询结果将存储到一级缓存中
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        // 先存储占位符号
        localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
        try {
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            // 删除占位符号
            localCache.removeObject(key);
        }
        // 将查询的数据存入当前缓存
        localCache.putObject(key, list);
        return list;
    }

     // 事务提交完毕清理缓存
    @Override
    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new RuntimeException("Cannot commit, transaction is already closed");
        }
        // step-17添加
        clearLocalCache();
        if (required) {
            transaction.commit();
        }
    }

      // 事务回滚完毕清理缓存
    @Override
    public void rollback(boolean required) throws SQLException {
        if (!closed) {
            // step-17添加
            try {
                clearLocalCache();
            } finally {
                if (required) {
                    transaction.rollback();
                }
            }
        }
    }

     // step-17添加,清理一级缓存
    @Override
    public void clearLocalCache() {
        if (!closed) {
            localCache.clear();
        }
    }

     /**
     * 创建缓存Key的hash,MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码作为 Key 使用。
     */
    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (closed) {
            throw new RuntimeException("Executor was closed.");
        }
        CacheKey cacheKey = new CacheKey();
        // sql的id
        cacheKey.update(ms.getId());
        // offset 
        cacheKey.update(rowBounds.getOffset());
        // limit 
        cacheKey.update(rowBounds.getLimit());
        // SQL 
        cacheKey.update(boundSql.getSql());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        // 根据参数获取参数的值
        for (ParameterMapping parameterMapping : parameterMappings) {
            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 {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            // queryParams
            cacheKey.update(value);
        }
        // 环境id
        if (configuration.getEnvironment() != null) {
            cacheKey.update(configuration.getEnvironment().getId());
        }
        return cacheKey;
    }
}

4.3 缓存操作

包:package cn.bugstack.mybatis.cache;

Cache接口:缓存的接口,这里提供了保存缓存、删除缓存、查询缓存、清除缓存、获取缓存长度等定义的方法。

/**
 * @Author df
 * @Description: SPI(Service Provider Interface) for cache providers. 缓存接口
 * 缓存接口主要提供了数据的存放、获取、删除、情况,以及数量大小的获取。这样的实现方式和我们通常做业务开发时,定义的数据存放都是相似的。
 * @Date 2024/1/9 10:24
 */
// step 17添加
public interface Cache {
    /**
     * 获取ID,每个缓存都有唯一ID标识
     */
    String getId();

    /**
     * 存入值
     */
    void putObject(Object key, Object value);

    /**
     * 获取值
     */
    Object getObject(Object key);

    /**
     * 删除值
     */
    Object removeObject(Object key);

    /**
     * 清空
     */
    void clear();

    /**
     * 获取缓存大小
     */
    int getSize();
}

包名:package cn.bugstack.mybatis.cache.impl;

PerpetualCache:一级缓存类,实现Cache的接口,把缓存放入到Map中就可以其他操作拉。

/**
 * @Author df
 * @Description: 一级缓存,在 Session 生命周期内一直保持,每创建新的 OpenSession 都会创建一个缓存器 PerpetualCache,
 * 一级缓存实现类也叫永久缓存
 * @Date 2024/1/9 10:26
 */
// step 17添加
public class PerpetualCache implements Cache {

    private Logger logger = LoggerFactory.getLogger(PerpetualCache.class);

    private String id;

    // 使用HashMap存放一级缓存数据,session 生命周期较短,正常情况下数据不会一直在缓存存放
    private Map<Object, Object> cache = new HashMap<>();

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

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        Object obj = cache.get(key);
        if (null != obj) {
            logger.info("一级缓存 \r\nkey:{} \r\nval:{}", key, JSON.toJSONString(obj));
        }
        return obj;
    }

    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

    @Override
    public void clear() {
        cache.clear();
    }

    @Override
    public int getSize() {
        return cache.size();
    }
}

CacheKey:缓存Key操作类,这个方法主要是BaseExecutor里的query方法调用,主要是doUpdate()方法,这里边的代码都有说明,这个方法主要是将传过来的参数处理成hash,并把原参数放入updateList(后边equal对比使用)。

这里重写了equals和hashcode等方式,如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存Key重写了 equals 对比方法。

/**
 * @Author df
 * @Description: 缓存 Key,一般缓存框架的数据结构基本上都是 Key->Value 方式存储
 * MyBatis 对于其 Key 的生成采取规则为:[mappedStatementId + offset + limit + SQL + queryParams + environment]生成一个哈希码
 * @Date 2024/1/9 10:34
 */
// step 17添加
public class CacheKey implements Cloneable, Serializable {
    private static final long serialVersionUID = 1146682552656046210L;

    public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

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

    private int multiplier;
    private int hashcode;
    private long checksum;
    private int count;
    private List<Object> updateList;

    public CacheKey() {
        this.hashcode = DEFAULT_HASHCODE;
        this.multiplier = DEFAULT_MULTIPLYER;
        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) {
        if (object != null && object.getClass().isArray()) {
            int length = Array.getLength(object);
            for (int i = 0; i < length; i++) {
                Object element = Array.get(object, i);
                doUpdate(element);
            }
        } else {
            doUpdate(object);
        }
    }

    /**
     * 1.根据参数计算hash码值
     * 2.为了保证不重复处理计算最终的码值
     * 3.并将对象放入updateList集合中
     */
    private void doUpdate(Object object) {
        // 确保hashcode一直都是有的。
        int baseHashCode = object == null ? 1 : object.hashCode();

        // 为了跟踪缓存更新的次数。
        count++;
        // 为了计算一个累积的校验和,用于检测缓存数据的一致性。
        checksum += baseHashCode;
        // 引入一个与更新次数相关的权重或因子,影响最终的哈希值。
        baseHashCode *= count;
        // 最终的哈希码值,相乘计算保证了对象或其属性变化时,哈希码都会改变
        hashcode = multiplier * hashcode * baseHashCode;

        // 目的是为了跟存储的参数进行对比
        updateList.add(object);
    }

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

    /**
     * 如果遇到相同哈希值,避免对象重复,那么 CacheKey 缓存Key重写了 equals 对比方法。这也就为什么在 doUpdate
     * 计算哈希方法时,把对象添加到 updateList.add(object); 集合中,就是用于这里的 equal 判断使用。
     * */
    // 重写对象的equals方法,用于对象判断
    @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) {
            return false;
        }
        if (checksum != cacheKey.checksum) {
            return false;
        }
        if (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 (thisObject == null) {
                if (thatObject != null) {
                    return false;
                }
            } else {
                if (!thisObject.equals(thatObject)) {
                    return false;
                }
            }
        }
        return true;
    }

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

    // 将每个参数都以冒号形式拼接。
    @Override
    public String toString() {
        StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
        for (Object obj : updateList) {
            returnValue.append(':').append(obj);
        }

        return returnValue.toString();
    }

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

NullCacheKey:NULL值操作。

/**
 * @Author df
 * @Description: NULL值缓存Key
 * @Date 2024/1/9 10:36
 */
// step 17添加
public class NullCacheKey extends CacheKey{
    private static final long  serialVersionUID = 3704229911977019465L;
    public NullCacheKey() {
        super();
    }
}

4.4 其他更改

为了能够兼容使用,其他地方的更改。

ExecutionPlaceholder:ExecutionPlaceholder类,这个是在存储结果数据集前先存储此占位符。

包名:package cn.bugstack.mybatis.executor;

/**
 * @Author df
 * @Description: 占位符
 * @Date 2024/1/9 11:29
 */
public enum ExecutionPlaceholder {
    EXECUTION_PLACEHOLDER
}

MappedStatement:MappedStatement修改,此类添加私有的变量

public class MappedStatement {
    // step-17添加
    private boolean flushCacheRequired;

    public boolean isFlushCacheRequired() {
        return flushCacheRequired;
    }
}

DefaultSqlSession:此类selectList方法调用query方法时去掉Sql的参数。

public class DefaultSqlSession implements SqlSession {

      @Override
    public <E> List<E> selectList(String statement, Object parameter) {
        logger.info("执行查询 statement:{} parameter:{}", statement, JSON.toJSONString(parameter));
        MappedStatement ms = configuration.getMappedStatement(statement);
        try {
            // step-17修改删除参数Sql
            return executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
        } catch (SQLException e) {
            throw new RuntimeException("Error querying database.  Cause: " + e);
        }
    }
}

 

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

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

相关文章

【算法】基础算法001之双指针

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.数组分块&#xf…

光伏项目如何建设和施工?

光伏项目现在已经成为能源行业最受欢迎、最有潜力的项目了&#xff0c;不仅仅可以减少温室气体的排放&#xff0c;也能够促进新能源和经济的发展。那么该如何建设和实施一个成功的光伏项目呢&#xff1f; 首先&#xff0c;选址是光伏项目成功的关键。理想的光伏项目应位于阳光充…

MySQL的三种存储引擎 InnoDB、MyISAM、Memory

InnoDB 1). 介绍 InnoDB是一种兼顾高可靠性和高性能的通用存储引擎&#xff0c;在 MySQL 5.5 之后&#xff0c;InnoDB是默认的MySQL 存储引擎。 2). 特点 DML操作遵循ACID模型&#xff0c;支持事务&#xff1b; 行级锁&#xff0c;提高并发访问性能&#xff1b; 支持外键F…

neo4j图数据库的简单操作记录

知识图谱文件导出 首先停止运行sudo neo4j stop然后导出数据库 导出格式为&#xff1a; 具体命令如下sudo neo4j-admin database dump --to-path/home/ neo4j最后重启sudo neo4j start知识图谱外观修改 在网页点击节点&#xff0c;选中一个表情后点击&#xff0c;可修改其颜…

springBoot-简单实践

基本介绍 在理解了自动依赖&#xff0c;以及自动配置后&#xff0c; 我们来做一个简单的使用springBoot的了解。 1、引入场景依赖 例如 <!--添加web场景--> <!-- springBoot会自动管理依赖&#xff0c;并自动配置--><dependencies><depende…

如何在OpenWRT部署uhttpd搭建服务器实现远程访问本地web站点

文章目录 前言1. 检查uhttpd安装2. 部署web站点3. 安装cpolar内网穿透4. 配置远程访问地址5. 配置固定远程地址 前言 uhttpd 是 OpenWrt/LuCI 开发者从零开始编写的 Web 服务器&#xff0c;目的是成为优秀稳定的、适合嵌入式设备的轻量级任务的 HTTP 服务器&#xff0c;并且和…

代码随想录算法训练营第二十五天| 回溯总结

回溯是递归的副产品&#xff0c;只要有递归就会有回溯&#xff0c;所以回溯法也经常和二叉树遍历&#xff0c;深度优先搜索混在一起&#xff0c;因为这两种方式都是用了递归。 回溯算法能解决如下问题&#xff1a; 组合问题&#xff1a;N个数里面按一定规则找出k个数的集合排…

CentOs 环境下使用 Docker 部署 Ruoyi-Vue

CentOs 环境下使用 Docker 部署 Ruoyi-Vue RuoYi-Vue 项目下载地址 RuoYi-Vue: &#x1f389; 基于SpringBoot&#xff0c;Spring Security&#xff0c;JWT&#xff0c;Vue & Element 的前后端分离权限管理系统&#xff0c;同时提供了 Vue3 的版本 (gitee.com) Docker 部…

存储卷(数据卷)—主要是nfs方式挂载

1、定义 容器内的目录和宿主机的目录进行挂载 容器在系统上的生命周期是短暂的&#xff0c;一旦容器被删除&#xff0c;数据会丢失。k8s基于控制器创建的pod&#xff0c;delete相当于重启&#xff0c;容器的状态会恢复到原始状态。一旦回到原始状态&#xff0c;后天编辑的文件…

RocketMQ Dashboard可视化工具

RocketMQ Dashboard 将 RocketMQ的相关指标展示在web页面 &#xff0c;支持以可视化工具代替 Topic 配置、Broker 管理等命令行操作。 官方文档地址&#xff1a;RocketMQ Dashboard | RocketMQ 目录 1.下载安装 1.1 系统要求&#xff1a; 1.2 源码安装 1.3 访问页面 2.功…

记录仪可作为XCP从站进行数据转发

车辆数据采集系统通常包含多种数据采集设备、多路总线或传感器信号&#xff0c;为了集中监控和管理&#xff0c;需要将这些设备的实时数据传输到上位机。对此&#xff0c;我们将使用基于XCP&#xff08;Universal Measurement and Calibration Protocol&#xff09;协议的数据记…

干货抢先看:SOLIDWORKS阵列操作的技巧与要点

SOLIDWORKS软件中的阵列功能十分常用且强大。本文将介绍一些关于SOLIDWORKS阵列的技巧&#xff0c;以帮助您更加高效地应用该功能。 1.线性阵列方向识别度增强 想使用线性阵列打孔的时候&#xff0c;模型上没有可以选中的参考线作为阵列方向怎么办&#xff1f;使用圆柱面也可…

基于ssm智慧社区停车管理系统设计与实现【附源码】

基于ssm智慧社区停车管理系统设计与实现 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&am…

深入理解.NET框架中的CLR(公共语言运行时)

深入理解.NET框架中的CLR&#xff08;公共语言运行时&#xff09; 引言 .NET框架中的CLR&#xff08;公共语言运行时&#xff09;是.NET应用程序运行的核心。本文将继续探索CLR的核心功能&#xff0c;并详细介绍.NET程序启动时是如何自动加载关键的库和服务来提供这些功能的。…

2024在视频号开店怎么样?平台现状如下,有电商经验者优先!

我是王路飞。 现在开网店、做电商的平台有很多&#xff0c;但是有着绝对流量优势的&#xff0c;除了抖音之外就是视频号了。 但是抖音跟视频号相比&#xff0c;已经属于一个很成熟的平台了&#xff0c;商家们也开始进入到内卷阶段了。 所以&#xff0c;如果你们2024年想做电…

工业智能网关:HiWoo Box远程采集设备数据

工业智能网关&#xff1a;HiWoo Box远程采集设备数据 在工业4.0和智能制造的浪潮下&#xff0c;工业互联网已成为推动产业升级、提升生产效率的关键。而在这其中&#xff0c;工业智能网关扮演着至关重要的角色。今天&#xff0c;我们就来深入探讨一下工业智能网关。 一、什么…

学习笔记17——通俗易懂的三次握手四次挥手

提供一种博主本人觉得很好理解的三次握手和四次挥手场景&#xff0c;帮助记忆 三次握手过程 初始状态&#xff1a;客户端处于closed状态&#xff0c;服务器处于listen监听转台客户端向服务器发送一个SYN连接请求&#xff0c;并告诉对方自己此时初始化序列号为x&#xff0c;发送…

k8s的策略

集群调度&#xff1a; Scheduler的调度算法&#xff1a; 预算策略 过滤出合适的节点 优先策略 选择部署的节点 NodeName&#xff1a;硬策略&#xff0c;不走调度策略&#xff0c;node1 nodeSelector&#xff1a;根据节点的标签选择&#xff0c;会走一个调度算法 只要是…

MySQL:索引失效场景总结

1 执行计划查索引 通过执行计划命令可以查看查询语句使用了什么索引。 EXPLAIN SELECT * FROM ods_finebi_area WHERE areaName = 福建 执行查询计划后,key列的值就是被使用的索引的名称,若key列没有值表示查询未使用索引。 2 在什么列上创建索引 (1)列经常被用于where…

工程送样!手把手教你用好广和通RedCap模组FG131FG132系列

2024年1月&#xff0c;广和通RedCap模组FG131&FG132系列已进入工程送样阶段&#xff0c;可为终端客户提供样片。广和通RedCap模组系列满足不同终端对5G速率、功耗、尺寸、成本的需求&#xff0c;全面助力RedCap技术的行业应用。 FG131&FG132系列基于骁龙X35 5G调制解调…