Caffeine本地缓存快速上手教程,通俗易懂

1. 概述

使用缓存的优点是可以减少直接访问数据库的压力。Caffeine是目前单机版缓存性能最高的,提供了最优的缓存命中率。用法和java中的map集合比较类似,底层使用一个ConcurrencyHashMap来保存所有数据,可以理解为一个增强版的map集合,增强的功能有设置缓存过期时间,缓存数据驱逐,统计缓存数据等。本文会大量使用详细的代码示例,通俗易懂地帮助大家学会使用Caffeine本地缓存。

常见QA

  1. Caffeine和redis的区别?共同点都是基于内存。其中,Caffeine是本地缓存,基于单个JVM,不能直接跨多台机器分布,如果程序停止,JVM停止,本地缓存数据会全部丢失,类似java中的map集合,相比Redis,Caffeine的性能更好。Redis是一个分布式缓存系统,独立部署,支持将数据持久化到磁盘上,因此可以在应用程序关闭后仍然保留数据。Redis支持分布式架构,可以配置成主从模式或者集群模式,从而提供更好的水平扩展性和高可用性。
  2. Ehcache和Caffeine的区别?Caffeine是一个较新的本地缓存框架,在内存管理和高并发访问方面通常比Ehcache更高效。

2. 准备

引入依赖即可

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.3</version>
</dependency>

3.代码讲解

Caffeine 最核心的是com.github.benmanes.caffeine.cache.Cache接口,所有与缓存有关的处理方法,都是在这个接口之中定义的,接口中的方法见下图。
在这里插入图片描述

1. 入门代码

private static void demo01() throws InterruptedException {
       
        /**
         * .expireAfterAccess(3L, TimeUnit.SECONDS):假设用户 "Alice" 第一次登录,登录状态被存储在缓存中,并且记录了最后一次访问的时间。
         * 假设用户 "Alice" 第一次登录,登录状态被存储在缓存中,并且记录了最后一次访问的时间。
         * 如果在3秒内没有任何请求使用了 "Alice" 的登录状态,那么缓存中的 "Alice" 条目会在3秒后自动过期,即被移除。
         * 下次有请求需要使用 "Alice" 的登录状态时,缓存会失效,需要重新加载或计算 "Alice" 的登录状态,并将新的状态存储在缓存中
         * 这样设计的好处在于,如果用户在一段时间内没有活动(例如3秒内没有操作),那么缓存中的数据会自动过期,可以确保缓存中的数据不会长时间驻留,
         * 从而减少缓存占用的内存空间,并且确保了数据的及时更新。
         */
        /**
         * 在设置了 .maximumSize(100) 之后,如果缓存中的条目数量超过了100,Caffeine 缓存库会根据一定的策略来进行缓存条目的淘汰,
         * 以确保缓存的大小不会无限增长。
         */
        Cache<String, String> cache = Caffeine.newBuilder()     // 构建一个新的caffeine实例
                                .maximumSize(100)   // 设置缓存之中保存的最大数据量
                                .expireAfterAccess(3L, TimeUnit.SECONDS)    // 缓存数据在3秒内没被访问则失效
                                .build();
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        log.info("未超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));  // 获取数据,输出:未超时获取缓存数据,Bob= 已登录
        TimeUnit.SECONDS.sleep(5);  // 5秒后超时
        log.info("已超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));  // 获取数据,输出:已超时获取缓存数据,Bob= null
        /**
         * 在默认情况下,一旦缓存数据消失之后,Cache 接口可以返回的内容就是 null 数据了,于是有些人认为空数据不利于标注,
         * 那么此时也可以考虑进行一些数据的控制。
         * 这种数据加载操作指的是在缓存数据不存在的时候进行数据的同步加载处理操作
         */
        log.info("已超时获取缓存数据,Bob= {}", cache.get("Bob", (key) -> {  // 最终输出:已超时获取缓存数据,Bob= [expire]Bob
            log.info("失效处理,没有发现 key = {} 的数据,要进行失效处理控制", key);
            return "[expire]" + key;  // 失效数据的返回
        }));
    }

2. 同步数据加载

数据加载是指将数据放入缓存的过程
如果发现指定的 KEY 的缓存项不存在了,Caffeine提供相关功能,实现重新进行数据的加载,例如:通过demo01方法中的操作方法可以发现,此时当缓存数据失效之后,可以自动的根据 Function 函数式接口加载所需要的数据内容(demo01中cache.get(“Bob”, (key) -> { }代码部分)
同步数据加载操作属于同步的操作范畴,加载不停,数据是不会返回的(所有操作均由主线程顺序执行)。而除了上文入门案例demo01中的加载机制之外,在缓存组件之中还提供有一个较为特殊的 CacheLoader 接口,这个接口的触发机制有些不太一样,它所采用的依然是同步的加载处理。

private static void demo02(){
        LoadingCache<String, String> cache = Caffeine.newBuilder()     // 第四步,修改变量类型为LoadingCache
                .maximumSize(100)   // 设置缓存之中保存的最大数据量
                .expireAfterAccess(3L, TimeUnit.SECONDS)    // 缓存数据在3秒内没被访问则失效
                .build(new CacheLoader<String, String>() {  // 第一步,build方法中传参
                    @Override
                    public @Nullable String load(@NonNull String s) throws Exception {
                        log.info("[cacheLoader]进行缓存数据的加载处理, 当前的key = {}", s);
                        TimeUnit.SECONDS.sleep(1);  // 第三步,模拟数据的加载延迟
                        return "【loadingcache】" + s;  // 第二步,数据加载的返回结果
                    }
                });
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        cache.put("Wang", "未登录");

        log.info("未超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));  // 未超时获取缓存数据,Bob= 已登录
        try {
            TimeUnit.SECONDS.sleep(5);  // 5秒后超时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        cache.put("Lee", "未登录");  // 第五步,缓存失效以后添加新的数据项
        List<String> list = new ArrayList<>();  // 第六步,封装一个list
        list.add("Bob");
        list.add("Lily");
        list.add("Lee");
        for (Map.Entry<String, String> entry : cache.getAll(list).entrySet()){  // 第七步
            log.info("【数据加载】key={},value={}", entry.getKey(), entry.getValue());
        }
        /**
         * 第八步,返回结果
         * 未超时获取缓存数据,Bob= 已登录
         * [cacheLoader]进行缓存数据的加载处理, 当前的key = Bob
         * [cacheLoader]进行缓存数据的加载处理, 当前的key = Lily
         * 【数据加载】key=Bob,value=【loadingcache】Bob  
         * 【数据加载】key=Lily,value=【loadingcache】Lily  
         * 【数据加载】key=Lee,value=未登录
         */
    }

3. 数据的异步加载操作

多线程可以提升性能,优先,功能和上面的同步加载数据相同
数据加载进内存的过程是异步的,从缓存中读数据默认还是由主线程同步实现。
注意:缓存的value应为CompletableFuture.completedFuture(“value”)格式

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()     
                .maximumSize(100)   // 设置缓存之中保存的最大数据量
                .expireAfterAccess(3L, TimeUnit.SECONDS)    // 缓存数据在3秒内没被访问则失效
                .buildAsync((key, executor) ->
                    CompletableFuture.supplyAsync(() -> {
                        log.info("[cacheLoader]进行缓存数据的加载处理, 当前的key = {}", key);
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return "【loadingcache】" + key;
                    })
                );
        cache.put("Bob", CompletableFuture.completedFuture("已登录"));
        cache.put("Lily",CompletableFuture.completedFuture("未登录"));
        cache.put("Wang", CompletableFuture.completedFuture("未登录"));

        log.info("未超时获取缓存数据,Bob= {}", cache.getIfPresent("Bob").get());  // 未超时获取缓存数据,Bob= 已登录
        try {
            TimeUnit.SECONDS.sleep(5);  // 5秒后超时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        cache.put("Lee", CompletableFuture.completedFuture("已登录"));  
        List<String> list = new ArrayList<>(); 
        list.add("Bob");
        list.add("Lily");
        list.add("Lee");
        for (Map.Entry<String, String> entry : cache.getAll(list).get().entrySet()){  
            log.info("【数据加载】key={},value={}", entry.getKey(), entry.getValue());
        }
        /**
         * 结果输出
         * [main] INFO com.cabbage.demos.AsynJiazai - 未超时获取缓存数据,Bob= 已登录
         * [ForkJoinPool.commonPool-worker-2] INFO com.cabbage.demos.AsynJiazai - [cacheLoader]进行缓存数据的加载处理, 当前的key = Bob
         * [ForkJoinPool.commonPool-worker-11] INFO com.cabbage.demos.AsynJiazai - [cacheLoader]进行缓存数据的加载处理, 当前的key = Lily
         * [main] INFO com.cabbage.demos.AsynJiazai - 【数据加载】key=Bob,value=【loadingcache】Bob
         * [main] INFO com.cabbage.demos.AsynJiazai - 【数据加载】key=Lily,value=【loadingcache】Lily
         * [main] INFO com.cabbage.demos.AsynJiazai - 【数据加载】key=Lee,value=已登录
         */

4.缓存数据驱逐

默认的缓存驱逐算法是Window-TinyLFU,提供了最优命中率,有效避免热点数据的失效。
以下代码示例都是基于同步缓存数据加载,

4.1基于缓存容量的驱逐策略

  • 假设缓存容量设置为1,当你设置第二条数据时,第一条数据丢失
private static void demo01() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()     // 构建一个新的caffeine实例
                .maximumSize(1)   // 设置缓存之中保存的最大数据量
                .expireAfterAccess(3L, TimeUnit.SECONDS)    // 缓存数据在3秒内没被访问则失效
                .build();
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        TimeUnit.MILLISECONDS.sleep(10);
        /**
           有一些延迟
         * 如果不加sleep,会出现
         * 现在缓存个数已经超过了,但是最早的缓存数据还在保留,没有及时清理
         * 大家可以去掉sleep自己试一下
         */
        log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));  // 输出:获取缓存数据,Bob= null
        log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily")); //输出:获取缓存数据,Lily= 未登录
    }

4.2 基于缓存权重驱逐策略

  • 先设置一个总的权重,再为每一条数据定义权重,例如:假设总权重为100,为每条数据设置权重50,那么在你设置第三条数据的时候,会有一条缓存数据被淘汰。
    private static void demo02(){
        Cache<String, String> cache = Caffeine.newBuilder()
                // .maximumSize(100)
                /**
                 * 在进行权重驱逐策略配置的时候,使用的方法为“maximumWeiaht()'
                 * 但是此时不要再设置保存的个数了,
                 * 因为个数的算法和权重的算法是两个不同的方式,二选一的关系。
                 */
                .maximumWeight(100)  // 第一步,设置缓存之中的最大权重
                .weigher((key, value) -> {  // 第二步,权重计算
                    log.info("[weigher权重计算器] key = {}, val = {}", key, value);
                    // 实际开发之中的权重计算处理操作,可以通过KEY和VALUE的长度计算得来
                    return 50; // 第三步
                })
                .expireAfterAccess(3L, TimeUnit.SECONDS)    // 缓存数据在3秒内没被访问则失效
                .build();
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        cache.put("Wang","未登录");
        cache.put("Lee","已登录");

        log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
        log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
        log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
        log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));
        /**
         * 输出
         *  [weigher权重计算器] key = Bob, val = 已登录
         *  [weigher权重计算器] key = Lily, val = 未登录
         *  [weigher权重计算器] key = Wang, val = 未登录
         *  [weigher权重计算器] key = Lee, val = 已登录
         * 获取缓存数据,Bob= null
         * 获取缓存数据,Lily= null
         * 获取缓存数据,Wang= 未登录
         * 获取缓存数据,Lee= 已登录
         */
    }

4.3 基于时间的驱逐策略

  • 在进行驱逐的时候,对于时间的管理有两种,一种是通过最后一次读的方式进行配置(见入门代码.expireAfterAccess(3L, TimeUnit.SECONDS) // 缓存数据在3秒内没被访问则失效),另外一种就是通过写的时间进行计数(写完以后的第几秒,缓存会失效)。
private static void demo03() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(2L, TimeUnit.SECONDS)    // 第一步,写入后两秒失效
                .build();
        cache.put("Bob", "已登录");
        for (int i = 0;i<3;i++){
            TimeUnit.MILLISECONDS.sleep(1500); // 每次休眠1.5秒
            log.info("[第{}次访问] key = {}, value = {}", i, "Bob", cache.getIfPresent("Bob"));
        }
        /**
         * 输出
         * 14:34:49.972 [main] INFO com.cabbage.demos.CacheEvictionManager - [第0次访问] key = Bob, value = 已登录
         * 14:34:51.478 [main] INFO com.cabbage.demos.CacheEvictionManager - [第1次访问] key = Bob, value = null
         * 14:34:52.989 [main] INFO com.cabbage.demos.CacheEvictionManager - [第2次访问] key = Bob, value = null
         */
    }

4.4 采用定制化的缓存驱逐策略

  • 可以通过 Expiry 接口来实现,这个接口内部定义有如下的处理方法:expireAfterCreate,expireAfterUpdate,expireAfterRead,详细实现参考以下代码
private static void demo04() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfter(new Expiry<String, String>() {
                    @Override
                    public long expireAfterCreate(@NonNull String s, @NonNull String s2, long l) {
                        log.info("[创建后失效计算 key = {}, value = {}]", s, s2);
                        // 相当于创建后多少秒就失效了
                        return TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS);
                    }

                    @Override
                    public long expireAfterUpdate(@NonNull String s, @NonNull String s2, long l, @NonNegative long l1) {
                        log.info("[更新后失效计算 key = {}, value = {}]", s, s2);
                        // 更新完多少秒后就失效了
                        return TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS);
                    }

                    @Override
                    public long expireAfterRead(@NonNull String s, @NonNull String s2, long l, @NonNegative long l1) {
                        log.info("[读取后失效计算 key = {}, value = {}]", s, s2);
                        // 读取完多少秒后就失效了
                        return TimeUnit.NANOSECONDS.convert(5, TimeUnit.SECONDS);  // 将2秒转成纳秒
                    }
                })   // 第一步
                .build();
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        cache.put("Wang","未登录");
        cache.put("Lee","已登录");

        log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
        log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
        log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
        log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));

        /**
         *
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Bob, value = 已登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Lily, value = 未登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Wang, value = 未登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [创建后失效计算 key = Lee, value = 已登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Bob, value = 已登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Bob= 已登录
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Lily, value = 未登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Lily= 未登录
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Wang, value = 未登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Wang= 未登录
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - [读取后失效计算 key = Lee, value = 已登录]
         * 14:51:23.040 [main] INFO com.cabbage.demos.CacheEvictionManager - 获取缓存数据,Lee= 已登录
         */
    }

5. 缓存数据的删除与监听

  • 手动删除一条数据
    private static void demo01() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .build();
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        cache.put("Wang","未登录");
        cache.put("Lee","已登录");

        cache.invalidate("Bob");  // 第一步,删除指定key的缓存
        // cache.invalidateAll();  // 删除所有缓存

        log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
        log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
        log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
        log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));
        /**
         * 输出结果
         * 11:20:37.978 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Bob= null
         * 11:20:37.980 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lily= 未登录
         * 11:20:37.980 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Wang= 未登录
         * 11:20:37.980 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lee= 已登录
         */
    }
  • 删除监听,删除数据之前可以通过监听进行一些操作
private static void demo02() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .removalListener(new RemovalListener<String, String>() {  // 第一步,设置监听器,删除时触发,removalCause是删除的原因
                    @Override
                    public void onRemoval(@Nullable String s, @Nullable String s2, @NonNull RemovalCause removalCause) {
                        log.info("【数据删除监听】key = {}, value = {}, cause = {}", s, s2, removalCause);
                    }
                })
                .build();
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        cache.put("Wang","未登录");
        cache.put("Lee","已登录");

        cache.invalidate("Bob");  // 删除指定key的缓存
        // cache.invalidateAll();  // 删除所有缓存

        log.info("获取缓存数据,Bob= {}", cache.getIfPresent("Bob"));
        log.info("获取缓存数据,Lily= {}", cache.getIfPresent("Lily"));
        log.info("获取缓存数据,Wang= {}", cache.getIfPresent("Wang"));
        log.info("获取缓存数据,Lee= {}", cache.getIfPresent("Lee"));
    }
    /**
     * 输出结果
     * 11:19:46.021 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Bob= null
     * 11:19:46.021 [ForkJoinPool.commonPool-worker-9] INFO com.cabbage.demos.CacheDelAndListener - 【数据删除监听】key = Bob, value = 已登录, cause = EXPLICIT
     * 11:19:46.024 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lily= 未登录
     * 11:19:46.024 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Wang= 未登录
     * 11:19:46.024 [main] INFO com.cabbage.demos.CacheDelAndListener - 获取缓存数据,Lee= 已登录
     */

6. CacheStats 缓存数据统计

/**
     * 获取缓存的 统计数据
     * 在使用数据统计的时候,Caffeine 内部使用了一个 StatsCounter 接口类型,
     * 最终如果要想实现数据的统计的处理操作,那么肯定是需要通过 StatsCounter 接口实现的,而这个接口提供有一个内置的并
     * 发数据统计的操作实现子类。
     * Cafeine 缓存组件除了提供有强大的缓存处理性能之外,也额外提供了一些缓存数据的统计功能,每当用户进行缓存数据操下时,
     * 都可以对这些操作记录的结果进行记录,这样就可以准确的知道缓存命中数、失效数、驱逐数等统计结果。
     */
    private static void demo01() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(1L, TimeUnit.MILLISECONDS)  // 设置1毫秒未读过期
                .recordStats()
                .build();
        cache.put("Bob", "已登录");
        cache.put("Lily","未登录");
        cache.put("Wang","未登录");
        cache.put("Lee","已登录");

        // 此时设置的候选的KEY数据是有些不存在的,通过这些不存在的数据进行最终的非命中统计操作
        String[] keys = new String[]{"Bob", "Lily", "Wang", "Lee", "No1", "no2"};
        // 定义随机数
        Random random = new Random();
        for (int i=0;i<1000;i++){
            new Thread(() -> {
                String key = keys[random.nextInt(keys.length)]; // 随机选取一个key
                log.info("key = {}, value = {}", key,cache.getIfPresent(key));
            },"查询线程 - "+i).start();
        }
        TimeUnit.SECONDS.sleep(1);  // 让多线程执行完
        CacheStats stats = cache.stats();
        log.info("【CacheStats】缓存操作请求次数: {}", stats.requestCount());
        log.info("【CacheStats】缓存命中次数: {}", stats.hitCount());
        log.info("【Cachestats】缓存未命中次数: {}", stats.missCount());
        //所有的缓存组件里面,最为重要的一项性能指标就是命中率的处理问题了
        log.info("【CacheStats】缓存命中率: {}", stats.hitRate());
        log.info("【CacheStats】缓存驱逐次数: {}", stats.evictionCount());
        /**
         * 输出结果
         16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存操作请求次数: 1000
         16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存命中次数: 13
         16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【Cachestats】缓存未命中次数: 987
         16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存命中率: 0.013
         16:20:02.911 [main] INFO com.cabbage.demos.CacheStatsDemo - 【CacheStats】缓存驱逐次数: 4

         */
    }

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot的“留守儿童爱心网站”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“留守儿童爱心网站”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统首页界面图 宣传新闻界面图 志愿活动界面…

基于Spring Boot的校园管理系统 ,计算机毕业设计(带源码+论文)

源码获取地址&#xff1a; 码呢-一个专注于技术分享的博客平台一个专注于技术分享的博客平台,大家以共同学习,乐于分享,拥抱开源的价值观进行学习交流http://www.xmbiao.cn/resource-details/1767745870094217218

立式学习灯有什么讲究?大路灯原来要这样选,五大台灯分享!

立式学习灯作为近年来最适合照明的护眼家电&#xff0c;为用户提供了良好的光线环境&#xff0c;并且还能够减少光线带来的视觉疲劳感。然而&#xff0c;随着其销量的节节攀升商家为了谋取利润&#xff0c;市面上也涌现了很多劣质产品&#xff0c;这些产品普遍没有经过技术调教…

BEC报考公告 ,柯桥成人学商务英语,商务英语口语学校

BEC报考公告 报名时间 2024年3月12日10:00——2023年3月20日10:00 注册个人信息、上传电子照片并支付考试费用 考试时间 BEC初级&#xff1a;5月12日 BEC中级&#xff1a;5月25日 BEC高级&#xff1a;5月18日 笔试及口试具体时间以准考证为准 报名费用 初级&#xff1a;…

一键美化ppt的ai工具有哪些?推荐5款自动生成PPT的ai软件!

伴随着 ai 人工智能技术的发展&#xff0c;作为普通个体的我们&#xff0c;也有机会享受到技术发展带来的红利&#xff0c;比如用 ai 来润色文章&#xff0c;用 ai 来美化 ppt 演示文稿&#xff0c;用 ai 辅助进行数据分析等等&#xff0c;ai 的应用场景多种多样&#xff0c;使…

加速布局Wi-Fi及蓝牙市场!移远通信再推四款高性能模组新品

3月12日&#xff0c;全球领先的物联网整体解决方案供应商移远通信宣布&#xff0c;其已正式推出四款新型Wi-Fi和蓝牙模组新品&#xff0c;旨在继续致力于满足物联网行业不断升级的应用需求&#xff0c;为智慧家居、工业互联、储能、充电桩等各种场景提供一站式创新解决方案。 此…

【阿里云系列】-ACK的Java应用POD无法访问云数据库Redis

问题介绍 如下图所示&#xff0c;是ACK集群的POD访问阿里云的云数据库Redis&#xff0c;如何实现访问呢 配置步骤 要实现ACK集群内的所有POD都可以访问云数据库Redis&#xff0c;则需要在Redsi的白名单里增加源IP或网段&#xff0c;如下图所示 注意&#xff1a; 以上添加…

朴素贝叶斯算法基础——案例:对新闻进行分类

贝叶斯公式 朴素&#xff1a;假设特征与特征之间相互独立 朴素贝叶斯算法&#xff1a;朴素贝叶斯 应用场景&#xff1a;文本分类&#xff08;单词作为特征&#xff09; 拉普拉斯平滑系数 Ni&#xff1a;F1词在C类别所有文档中出现的次数 N&#xff1a;所属类别C下的文档所…

C语言 指针(4) qsort函数

目录 前言 一、回调函数 二、qsort函数 2.1 使用qsort函数排序整型数据 2.2 使用qsort排序结构数据 三、qsort函数的模拟实现 总结 前言 今天我们主要来学习一下C语言中的qsort排序函数。 一、回调函数 回调函数就是⼀个通过函数指针调用的函数。 如果你把函数的指针&a…

PMP考试心得,与大家共勉

本人刚刚通过PMP考试&#xff0c;有一些自己的经历&#xff0c;写出来欢迎已经是PMP的兄弟们指正&#xff0c;希望能给正在PMP之路上奔跑的人们一些帮助。 其实很早就听说过PMP认证考试了&#xff0c;但是一直工作很忙没有时间来投入学习考试。因此一直拖到今年夏天才有时间参加…

使用公式在Excel中指定列值的变化实现自动间隔着色(不是按照固定的行数)

如果你的文件很小&#xff0c;可以手工着色&#xff1b;但如果很大&#xff0c;就要借助公式来着色&#xff1b; 目的是什么&#xff0c;其中之一是&#xff1a;提升可读性。 一起往下看吧&#xff01;&#xff01; 如果你想要根据Excel某列中值的变化来间隔着色&#xff0c;…

CSS 【详解】响应式布局(明天内容)

响应式布局&#xff1a; 同一页面在不同的屏幕上有不同的布局&#xff0c;即一套代码自适应不同的屏幕。 常用 单位&#xff1a; 像素&#xff08;px&#xff09;&#xff1a;像素是最常用的长度单位&#xff0c;它表示屏幕上的一个物理像素点。例如&#xff0c;width: 200px; …

迪杰斯特拉算法 代码

参考链接&#xff1a; 【路径规划】全局路径规划算法——Dijkstra算法&#xff08;含python实现 | c实现&#xff09;-CSDN博客 算法图解&#xff1a; 代码 def dijkstra(matrix, source):"""迪杰斯特拉算法实现Args:matrix (_type_): 用邻接矩阵表示带权图s…

代码随想录算法训练营第六天| 242.有效字母的异位词、349.两个数组的交集、202快乐数、1.两数之和

系列文章目录 目录 系列文章目录242.有效的字母异位词349. 两个数组的交集①使用HashSet②使用Hash数组 202. 快乐数1. 两数之和①暴力解法&#xff08;时间复杂度不符合要求&#xff09;②使用HashMap法 242.有效的字母异位词 这道题是数组在哈希表中的典型应用。 因为只有2…

【C++】STL(七) set容器

8. set容器8.1 简介8.2 构造和赋值例子 8.3 大小和交换例子 8.4 插入和删除例子 8.5 查找和统计例子 8.6 set和multiset区别例子 8.7 pair对组创建 ----- 成对出现的数据&#xff0c;利用对组可以返回两个数据创建方式例子 8.8 内置类型指定排序规则&#xff08;1&#xff09; …

Powershell应用

Powershell应用 帮助命令进程管理服务管理文件管理网络管理系统管理用户管理远程管理常见问题 字符串和文本处理脚本和模块其他常用命令返回值类型PowerShell调用C# 类库PowerShell使用WmiWQL测试工具 帮助命令 Get-Help 这个命令用于获取其他命令的帮助文档&#xff0c;例如 …

像SpringBoot一样使用Flask - 3.蓝图路由Blueprint

接上一篇文章《像SpringBoot一样使用Flask - 2.静态资源访问及模版》&#xff0c;我们看到测试的"controller"都写在了一起&#x1f914; 如何像Springboot一样划分出一个完整的controller&#xff0c;里面实现不同业务的包呢&#xff1f; 本篇引入Blueprint&#xf…

Qt教程 — 1.1 Linux下安装Qt

目录 1 下载Qt 1.1 官方下载 1.2 百度网盘下载 1.3 Linux虚拟机终端下载 2 Qt安装 3 安装相关依赖 4 测试安装 1 下载Qt 1.1 官方下载 通过官网下载对应版本&#xff0c;本文选择的版本为qt-opensource-linux-x64-5.12.12&#xff0c;Qt官方下载链接&#xff1a;htt…

Liunx文件系统和基础IO

文件系统和基础IO 基础IOc语言基础IO函数当前路径和标准流系统IO系统调用函数重定向FILE文件结构体 在谈缓存区问题理解文件系统初识inode 基础IO c语言基础IO函数 打开与关闭 FILE *fopen(char *filename, const char *mode);选项还可以是 r/w/a 意味着为可读可写打开。 2…

【CSS】 css 实现文字的渐变色

效果 实现 .text {position: absolute;left: 52px;top: 1px;width: 200px;height: 31px;font-family: YouSheBiaoTiHei;font-size: 24px;color: rgba(255, 255, 255, 0.8);line-height: 31px;text-shadow: 0px 0px 8px #000000;text-align: center;font-style: normal;transiti…