MyBatis源码学习四之二级缓存

news2025/2/24 22:13:42

MyBatis源码学习四之二级缓存

MyBatis的缓存使用的也比较多,而缓存的都实现了一个父类的接口Cache

在这里插入图片描述

一、加载缓存类PerputualCache

public static void main(String[] args) {
    InputStream inputStream = null;
    try {
        inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = factory.openSession();
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.getUserNameById("20");
        System.err.println(user.getId() + user.getPsnname());
        User user = mapper.getUserNameById("20");
        System.err.println(user.getId() + user.getPsnname());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    ...
    <mappers>
        <mapper resource="mapper/UserMapper.xml"></mapper>
    </mappers>
</configuration>

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yt.study.mybatis.UserMapper">

    <cache eviction="FIFO"></cache>

    <select id="getUserNameById" useCache="true">
        select * from user where id=#{id}
    </select>

</mapper>

SqlSessionFactoryBuilder.build

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

XMLConfigBuilder.parse

public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
private void parseConfiguration(XNode root) {
    try {
        // issue #117 read properties first
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        loadCustomLogImpl(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

关键点 mapperElement

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          // 进入到这里
          ErrorContext.instance().resource(resource);
          try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
                configuration.getSqlFragments());
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try (InputStream inputStream = Resources.getUrlAsStream(url)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,
                configuration.getSqlFragments());
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException(
              "A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

我们配置的是

<mappers>
    <mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>

所以会进入到解析mapper的resource流程中。

然后再看mapperParser.parse()

XMLMapperBuilder.parse

这里有解析cache-ref标签和cache标签的。

cache-ref标签的作用:多个命名空间中共享相同的缓存配置和实例。这里不看这个标签了。我们继续来看cache标签

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}

private void cacheElement(XNode context) {
    if (context != null) {
        // 类型:如果没有就是PERPETUAL
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 淘汰策略:LRU
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

BuilderAssistant.useNewCache

利用这个builder模式构建了一个Cache缓存,而在build方法中,还有一些初始化的东西。

public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval,
                         Integer size, boolean readWrite, boolean blocking, Properties props) {
    Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size)
        .readWrite(readWrite).blocking(blocking).properties(props).build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

CacheBuilder.build

public Cache build() {
    // 默认是PerpetualCache
    setDefaultImplementations();
    // 先实现化一个BaseCache出来。
    Cache cache = newBaseCacheInstance(implementation, id);
    // 这里则是支持对缓存中的属性进行赋值。而默认是PerpetualCache是没有属性的,所以不会进入,而使用自定义缓存的话,这里就用的上了。
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
        // 这里把setDefaultImplementations中增加的LruCache再进行包装一下
        for (Class<? extends Cache> decorator : decorators) {
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 添加一系列的标准装饰
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}

private void setDefaultImplementations() {
    if (implementation == null) {
        implementation = PerpetualCache.class;
        // 默认给装饰器列表增加了LruCache
        if (decorators.isEmpty()) {
            decorators.add(LruCache.class);
        }
    }
}
 
private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        // 清空数据时间不为空的话,增加ScheduledCache
        if (clearInterval != null) {
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        // boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        // 这里的属性是否可读,官网上的用的也是readOnly,在解析的时候,就变成了readWrite了,表意不够准确
        if (readWrite) {
            // 只读的话,增加SerializedCache
            cache = new SerializedCache(cache);
        }
        // 增加LoggingCache
        cache = new LoggingCache(cache);
        // 增加SynchronizedCache
        cache = new SynchronizedCache(cache);
        if (blocking) {
            // 阻塞的话,增加BlockingCache
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}

这么多层Cache装饰下来,最终就有一个这样的结构了。

在这里插入图片描述

到这里,缓存的初始化就完成了。下面我们看看怎么去使用的

二、使用二级缓存

我们来看看效果:只有第一次的时候,去查询了数据库,第二次没有去查询数据库,缓存命中了。

在这里插入图片描述

DefaultSqlSessionFactory.openSession

factory.openSession的时候,会去创建executor

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
                                             boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

Configuration.newExecutor

这里的判断条件有一个cacheEnabled,这个就是二级缓存的开关,默认打开着。这里就会用CacheExecutorSimpleExecutor进行包装。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    return (Executor) interceptorChain.pluginAll(executor);
}

DefaultSqlSession.selectOne

DefaultSqlSession创建好了后,再进入到DefaultSqlSession中,有个selectOne方法。因为我们是根据用户主键id去查询的 ,所以最终只能返回一条数据。肯定会进入到selectOne中。

多个重载后进入到selectList中。

@Override
public <T> T selectOne(String statement) {
    return this.selectOne(statement, null);
}
public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
        return list.get(0);
    }
    if (list.size() > 1) {
        throw new TooManyResultsException(
            "Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}

@Override
public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
}

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();
    }
}

CacheExecutor.query

executor的外层包装是CacheExecutor,所以会进入到CacheExecutor中。然后调用createCacheKey

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
    throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}

BaseExecutor.createCacheKey

这里会根据很多的参数以及条件去创建一个值当做缓存的key值。

@Override
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
  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 {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

CacheExecutor.query

再回到CacheExecutor.query中来 。第一个的时候,缓存中没有这个key的数据。则直接去查询DB,查到了之后,放入到缓存中。

以后再来查询的时候,就会直接根据cacheKey来去缓存中取数据。

而所有的增删改查都会默认的清空缓存的。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
                         CacheKey key, BoundSql boundSql) throws SQLException {
	// 这里的cache对象就是初始化的时候mapper中的cache:UserMapper.xml <cache />
    Cache cache = ms.getCache();
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            // 获取缓存,tcm.getObject
            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);
}

三、集成Redis:RedisCache

导入maven依赖。

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

核心类:RedisCache

package org.mybatis.caches.redis;

import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public final class RedisCache implements Cache {
    private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
    private String id;
    private static JedisPool pool;

    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        } else {
            this.id = id;
            RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
            pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
        }
    }

    private Object execute(RedisCallback callback) {
        Jedis jedis = pool.getResource();

        Object var3;
        try {
            var3 = callback.doWithRedis(jedis);
        } finally {
            jedis.close();
        }

        return var3;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return (Integer)this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                Map<byte[], byte[]> result = jedis.hgetAll(RedisCache.this.id.toString().getBytes());
                return result.size();
            }
        });
    }
	// jedis.hset
    public void putObject(final Object key, final Object value) {
        this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
                return null;
            }
        });
    }
	// jedis.hget
    public Object getObject(final Object key) {
        return this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                return SerializeUtil.unserialize(jedis.hget(RedisCache.this.id.toString().getBytes(), key.toString().getBytes()));
            }
        });
    }
	// jedis.hdel
    public Object removeObject(final Object key) {
        return this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                return jedis.hdel(RedisCache.this.id.toString(), new String[]{key.toString()});
            }
        });
    }
	// jedis.del
    public void clear() {
        this.execute(new RedisCallback() {
            public Object doWithRedis(Jedis jedis) {
                jedis.del(RedisCache.this.id.toString());
                return null;
            }
        });
    }

    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }

    public String toString() {
        return "Redis {" + this.id + "}";
    }
}

看过基于Redis实现的二级缓存后,我们是不是也可以自己用其他的中间键来实现呢?

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

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

相关文章

NRK3303语音识别芯片在照明灯上的运用,一款可分布式语音IC方案

随着科技的不断进步&#xff0c;人们对于家居生活中的照明设备的要求也逐渐提高。传统的照明方式已经不能满足人们对智能家居的需求&#xff0c;我们需要更加智能、易于操作、高效节能的智能化照明系统。因此&#xff0c;智能照明应运而生&#xff0c;为我们提供了更加智能化、…

倍福触摸屏维修控制面板CP6606-0001-0020

适合控制柜安装的CP6600和CP6606面板型PC&#xff0c;适用于机械制造和设备工程等各种应用&#xff1b;可以安装TwinCAT自动化软件和 Windows Embedded操作系统&#xff0c;用作单独的控制器&#xff0c;也可以用作远程桌面显示器。 倍福触摸屏常见故障分类&#xff1a; 1、磨…

linux学习[11]磁盘与文件系统(2):lsblkblkidpartedfdiskgdiskmkfs

文章目录 前言&#xff1a;1. 磁盘容量1.1 lsblk1.2 blkid1.3 parted 2. 磁盘分区2.1 fdisk/gdisk2.2 磁盘分区实例参考&#xff1a; 3. 磁盘格式化3.1 mkfs.xfs3.2 mkfs.ext43.3 mkfs.vfat 总结&#xff1a; 前言&#xff1a; 写了VMware的磁盘扩容之后&#xff0c;磁盘分区格…

深度学习基础-卷积神经网络CNN+深度学习(无代码仅理解)

参考书籍&#xff1a;&#xff08;找不到资源可以后台私信我&#xff09; 《深度学习入门&#xff1a;基于Python的理论与实现 (斋藤康毅)》 CNN 概括 其中pooling层有时候会被省略&#xff0c;卷积层的输入输出图像称为特征图&#xff08;feature map&#xff09;&#xff0c…

多线程-Thread类的常用方法和生命周期

Thread类的常用结构 构造器 public Thread():分配一个新的线程对象。public Thread(String name):分配一个指定名字的新的线程对象。public Thread(Runnable target):指定创建线程的目标对象&#xff0c;它实现了Runnable接口中的run()方法。public Thread(Runnable target,S…

Python实现温度植被干旱指数(TVDI)的计算

前言 温度植被干旱指数&#xff08;Temperature Vegetation Dryness Index&#xff0c;TVDI&#xff09;是一种基于光学与热红外遥感通道数据进行植被覆盖区域表层土壤水分反演的方法。作为同时与归一化植被指数(NDVI)和地表温度(LST)相关的温度植被干旱指数(TVDI)可用于干旱监…

第二十五节:通信之WLAN(WiFi聚合)

欢迎大家一起学习探讨通信之WLAN。为了减少帧交互中额外资源占用开销&#xff0c;提高WiFi网络系统整体运行效率&#xff0c;802.11n协议引入定义了聚合功能。本节将基于协议定义内容和实例&#xff0c;详细分析“A-MSDU"和“A-MPDU”两种聚合功能。 关键字 S1G(Sub 1 GH…

linux0.12-10-6-tty_io.c

[539页] 10-6 tty_io.c程序 10-6-1 功能描述 每个tty设备有3个缓冲队列&#xff0c;分别是读缓冲队列(read_q)、写缓冲队列(write_q)和辅助缓冲队列(secondary)&#xff0c;定义在tty_struct结构中(include/linux/tty.h)。 对于每个缓冲队列&#xff0c;读操作是从缓冲队列的…

数据可视化:部分整体类可视化图表大全

图表是处理数据的重要组成部分&#xff0c;因为它们是一种将大量数据压缩为易于理解的格式的方法。数据可视化可以让受众快速Get到重点。 数据可视化的图表类型极其丰富多样&#xff0c;而且每种都有不同的用例&#xff0c;通常&#xff0c;创建数据可视化最困难的部分是确定哪…

冯诺依曼体系结构详解

一.冯诺伊曼体系结构的概念&#xff1a; 约翰冯诺依曼&#xff08;John von Neumann&#xff0c;1903.1.28-1957.2.8&#xff09;&#xff0c;美籍匈牙利数学家&#xff0c;计算机科学家&#xff0c;物理学家。是20世纪最重要的数学家之一&#xff0c;后来被称为计算机之父。 后…

计算机网络学习笔记-网络层

目录 概述 提供的两种服务&#xff1a;面向连接的虚电路、不面向连接的数据报 对比 虚拟互连网络 地址解析协议 ARP 主要作用 使用过程 位置 因特网控制报文协议 ICMP 作用 位置 种类 差错报告报文&#xff1a;终点不可达、源点抑制、时间超过、参数问题、改变路由…

【HMS Core】【ML Kit】活体检测FAQ合集

【问题描述1】 使用示例代码集成活体检测SDK时&#xff0c;报错state code -7001 【解决方案】 使用示例代码前请详细阅读示例工程中的“README”文件。您需要完成以下操作后才可以运行示例代码。 在AppGallery Connect网站下载自己应用的“agconnect-services.json”文件&a…

kaggle新赛推荐 | 从游戏中预测学生的表现

赛题名称&#xff1a;Predict Student Performance from Game Play 从游戏中预测学生的表现 赛题链接&#xff1a;https://www.kaggle.com/competitions/predict-student-performance-from-game-play 赛题背景 学习意味着有趣&#xff0c;这就是基于游戏的学习的用武之地。这…

Java大型货运系统源码(司机APP端+货主APP端)

技术架构&#xff1a;spring boot、mybatis、redis、vue、element-ui 开发语言&#xff1a;java 开发工具&#xff1a;idea、vscode、hbuilder 前端框架&#xff1a;vue 后端框架&#xff1a;spring boot 数 据 库&#xff1a;mysql 移 动 端&#xff1a;uniapp混合开发原…

数据结构与算法(七)

二叉树 如果说树中的每个结点最多只能有两个子结点&#xff0c;这样的树我们就称为二叉树&#xff0c;二叉树可以为空。 特点: 每个结点最多有两棵子树&#xff0c;所以二叉树中不存在度大于二的结点棵树中&#xff0c;最大的结点的度称为树的度&#xff0c;结点的度:结点所…

Git 分支相关操作

1 创建一个分支 Create a new directory and initialize a Git repository. We are going to create a directory named “tutorial”. $ mkdir tutorial $ cd tutorial $ git init Initialized empty Git repository in /Users/eguchi/Desktop/tutorial/.git/进入这个tutori…

一篇文章全面了解光分路器、PLC分路器、拉锥分路器

光纤分路器 光纤分路器&#xff0c;又称为分光器&#xff0c;是将一根光纤信号按照既定的比例分解为两路或多路光信号输出&#xff0c;是接入FTTH方式的光无源器件。 例如&#xff0c;一个1x4光分路器就是将一根光纤中的光信号按照一定的比例分配给四根光纤。与WDM系统的波分复…

【Java入门】运算符

前言 &#x1f4d5;作者简介&#xff1a;热爱跑步的恒川&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步&#xff0c;喜爱音乐的一位博主。 &#x1f4d7;本文收录于Java入门篇系列&#xff0c;该专栏主要讲解&#xff1a;什么是java、java的数据类型与变…

放大镜-第14届蓝桥杯省赛Scratch中级组真题第3题

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第138讲。 放大镜&#xff0c;本题是2023年5月7日举行的第14届蓝桥杯省赛Scratch图形化编程中级组编程第3题&#xff0…

MAC环境下使用 xray 工具

这里不做过多介绍&#xff0c;下面链接讲的非常清楚&#xff0c;下面记录一下遇到的坑。 https://docs.xray.cool/#/tutorial/webscan_basic_crawler Mac环境下选择对应的工具 下载完以后&#xff0c;放入自己的目录下&#xff0c;打开终端查看版本信息 ./xray_darwin_amd64 v…