SpringBoot 使用 Cache 集成 Redis做缓存保姆教程

1. 项目背景

Spring Cache是Spring框架提供的一个缓存抽象层,它简化了缓存的使用和管理。Spring Cache默认使用服务器内存,并无法控制缓存时长,查找缓存中的数据比较麻烦。

因此Spring Cache支持将缓存数据集成到各种缓存中间件中。本文已常用的Redis作为缓存中间件作为示例,详细讲解项目中如何使用Cache提高系统性能。

2. Spring Cache介绍

Spring Cache是Spring框架提供的一种缓存解决方案,基于AOP原理,实现了基于注解的缓存功能,只需要简单地加一个注解就能实现缓存功能,对业务代码的侵入性很小。

使用Spring Cache的方法很简单,只需要在方法上添加注解即可实现将方法返回数据存入缓存,以及清理缓存等注解的使用。

2.1 主要特点

  1. 统一的缓存抽象:Spring Cache为应用提供了一种统一的缓存抽象,可以轻松集成各种缓存提供者(如Ehcache、Redis、Caffeine等),使用统一的API。
  2. 注解驱动:Spring Cache通过简单的注解配置,如@Cacheable@CachePut@CacheEvict等,可以快速实现缓存功能,而无需处理底层缓存逻辑。
  3. 灵活性和扩展性:Spring Cache允许根据业务需求自定义缓存策略,如缓存的失效时间、缓存的淘汰策略等。同时,它也提供了CacheManager接口和Cache接口,可以实现降低对各种缓存框架的耦合。

2.2 常用注解

@EnableCaching
  • 作用:开启Spring的缓存注解支持。
  • 使用场景:在配置类上添加此注解,以启用Spring Cache的注解处理功能。
  • 注意:此注解本身并不提供缓存实现,而是允许你使用@Cacheable@CachePut@CacheEvict等注解来定义缓存行为。
@Cacheable
  • 作用:在方法执行前检查缓存,如果缓存中存在数据则直接返回,否则执行方法并将结果缓存。
  • value:指定缓存的名称(或名称数组)。缓存名称与CacheManager中配置的缓存对应。
  • key:用于生成缓存键的表达式(可选)。如果不指定,则默认使用方法的参数值作为键。
  • condition:条件表达式(可选),用于决定是否执行缓存操作。
  • unless:否定条件表达式(可选),用于在方法执行后决定是否缓存返回值。
@Cacheable注解配置参数说明
  1. value/cacheNames

    • 用于指定缓存的名称(或名称数组),缓存名称作为缓存key的前缀。这是缓存的标识符,用于在CacheManager中查找对应的缓存。
    • valuecacheNames是互斥的,即只能指定其中一个。
  2. key

    • 用于生成缓存键的表达式。这个键用于在缓存中唯一标识存储的值。
    • 如果不指定key,则默认使用方法的参数值(经过某种转换)作为键。
    • 可以使用Spring Expression Language(SpEL)来编写key表达式,以实现动态键的生成。
  3. keyGenerator

    • 指定一个自定义的键生成器(实现 org.springframework.cache.interceptor.KeyGenerator 接口的类),用于生成缓存的键。与 key 属性互斥,二者只能选其一。
    • 如果同时指定了keykeyGenerator,则会引发异常,因为它们是互斥的。
    • 开发者可以编写自己的KeyGenerator实现,并将其注册到Spring容器中,然后在@Cacheable注解中引用。
  4. cacheManager

    • CacheManager表示缓存管理器,通过缓存管理器可以设置缓存过期时间。
    • 用于指定要使用的CacheManager。这是一个可选参数,通常不需要显式指定,因为Spring会默认使用配置的CacheManager。
    • 如果系统中配置了多个CacheManager,则需要通过此参数指定使用哪一个。
  5. cacheResolver

    • 缓存解析器,用于解析缓存名称并返回相应的Cache对象。这也是一个可选参数。
    • 类似于cacheManager,如果系统中配置了多个缓存解析逻辑,可以通过此参数指定使用哪一个。
  6. condition

    • 条件表达式,用于决定是否执行缓存操作。这是一个可选参数。
    • 条件表达式使用SpEL编写,如果表达式返回true,则执行缓存操作;否则不执行。
  7. unless

    • 否定条件表达式,用于在方法执行后决定是否缓存返回值。这也是一个可选参数。
    • condition类似,unless也使用SpEL编写,但它是在方法执行后才进行评估的。
    • 如果unless表达式返回true,则不缓存返回值;否则缓存。
  8. sync

    • 是否使用异步模式进行缓存操作。这是一个可选参数,通常不需要显式指定。
    • 在多线程环境中,如果多个线程同时请求相同的数据并触发缓存操作,使用异步模式可以避免线程阻塞和重复计算。

@Cacheable注解的这些参数是互斥或相互关联的,例如valuecacheNames不能同时指定,keykeyGenerator也不能同时指定。此外,cacheManagercacheResolver也是互斥的,因为它们都用于指定缓存的解析和管理方式。

对于前两个注解的应用:

    @Cacheable(cacheNames = "cache:cacheByKey", key = "#id")
    public Integer cacheByKey(@PathVariable("id") Integer id) throws InterruptedException {
        Thread.sleep(5000);
        log.info("执行了cacheByKey方法" + id);
        return id;
    }

看注释掉的那行,取缓存名称为cache:cacheByKey,参数id的值作为key,最终缓存key为:缓存名称+“::”+key,例如:上述代码id为123,最终的key为:cache:cacheByKey::123

SpEL(Spring Expression Language)是一种在 Spring 框架中用于处理字符串表达式的强大工具,它可以实现获取对象的属性,调用对象的方法操作。

  • 单个缓存名称@Cacheable(value = "myCache") 表示使用名为myCache的缓存。
  • 多个缓存名称@Cacheable(value = {"cache1", "cache2"}) 表示方法的结果将同时缓存到cache1cache2中。
  • @CacheConfig结合使用:如果类上使用了@CacheConfig注解,并且指定了cacheNames属性,那么类中的方法在使用@Cacheable时可以省略value属性,直接使用类级别的缓存配置。
@CacheEvict
  • 作用:从缓存中删除数据。
  • value:指定要删除的缓存的名称(或名称数组)。
  • key:用于指定要删除的缓存键(可选)。如果不指定,则默认使用方法的参数值作为键。
  • allEntries:布尔值,指定是否删除缓存中的所有条目(而不是仅删除与指定键匹配的条目)。
  • beforeInvocation:布尔值,指定是否在方法执行之前删除缓存(默认为false,即在方法执行之后删除)。
@CachePut
  • 作用:更新缓存中的数据,无论方法是否成功执行,都会将结果放入缓存。
  • valuekeyconditionunless:与@Cacheable中的这些属性相同。
@Caching
  • 作用:允许在同一个方法上组合使用多个缓存注解(如@Cacheable@CachePut@CacheEvict)。
  • 属性:包含一个或多个缓存注解。
@CacheConfig
  • 作用:为类级别提供缓存相关的默认配置。
  • cacheNames:指定该类中所有方法使用的默认缓存名称(或名称数组)。
  • keyGenerator:指定自定义的键生成器(可选)。
  • cacheManager:指定要使用的CacheManager(可选)。

3. 示例代码

项目依赖于Redis配置,这里就不多赘述了。

缓存管理器配置:

定义了两个缓存管理器,默认cacheManager(使用@Primary标注),一个缓存返回值为null的管理器cacheNullManager,详情看下面代码。

package com.maple.redis.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.time.Duration;

/**
 * @author 笑小枫
 * @date 2025/1/7
 */
@Slf4j
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

    /**
     * 默认缓存管理器
     * 只有CacheManger才能扫描到cacheable注解
     * spring提供了缓存支持Cache接口,实现了很多个缓存类,其中包括RedisCache。但是我们需要对其进行配置,这里就是配置RedisCache
     */
    @Bean
    @Primary
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.RedisCacheManagerBuilder
                //Redis链接工厂
                .fromConnectionFactory(redisConnectionFactory)
                //缓存配置 通用配置  默认存储一小时
                .cacheDefaults(getCacheConfigurationWithTtl(Duration.ofHours(1)))
                //配置同步修改或删除  put/evict
                .transactionAware()
                //对于不同的cacheName我们可以设置不同的过期时间
                .withCacheConfiguration("cache2:cacheByUser", getCacheConfigurationWithTtl(Duration.ofHours(2)))
                .build();
    }

    /**
     * 创建并返回一个CacheManager Bean,用于管理Redis缓存。
     * 主要返回结果为null时使用,会缓存null值,缓存时间为10分钟,防止缓存穿透。
     * 使用时通过 cacheManager = "cacheNullManager" 指定使用该缓存管理器。
     */
    @Bean
    public CacheManager cacheNullManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.RedisCacheManagerBuilder
                //Redis链接工厂
                .fromConnectionFactory(redisConnectionFactory)
                //缓存配置 通用配置  默认存储一小时
                .cacheDefaults(RedisCacheConfiguration
                        .defaultCacheConfig()
                        // 设置key为String
                        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                        // 设置value 为自动转Json的Object
                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()))
                        .entryTtl(Duration.ofMinutes(10)))
                //配置同步修改或删除  put/evict
                .transactionAware()
                .build();
    }

    /**
     * 缓存的基本配置对象
     */
    private RedisCacheConfiguration getCacheConfigurationWithTtl(Duration duration) {
        return RedisCacheConfiguration
                .defaultCacheConfig()
                //设置key value的序列化方式
                // 设置key为String
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value 为自动转Json的Object
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()))
                // 不缓存null
                .disableCachingNullValues()
                // 设置缓存的过期时间
                .entryTtl(duration);
    }

    /**
     * 缓存的异常处理
     */
    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        // 异常处理,当Redis发生异常时,打印日志,但是程序正常走
        log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
            }

            @Override
            public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
                log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                log.error("Redis occur handleCacheClearError:", e);
            }
        };
    }

    @Override
    @Bean("myKeyGenerator")
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuffer sb = new StringBuffer();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }
}

使用案例:

User对象就idname两个字段,大家随意~

package com.maple.redis.controller;

import com.maple.redis.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.web.bind.annotation.*;

/**
 * @author 笑小枫
 * @date 2025/1/7
 */
@Slf4j
@RestController
@RequestMapping("/cache")
public class TestCacheController {

    /**
     * 获取简单缓存数据。
     *
     * <p>通过@Cacheable注解,该方法的结果会被缓存到名为"cache:simpleCache"的缓存中。
     * 如果在缓存中找到相同请求的结果,将直接返回缓存的值,避免重复执行方法体中的逻辑。
     *
     * <p>方法内部,使用Thread.sleep(5000)模拟了一个耗时操作,
     */
    @GetMapping("/simpleCache")
    @Cacheable(cacheNames = "cache:simpleCache")
    public String simpleCache() throws InterruptedException {
        Thread.sleep(5000);
        log.info("执行了simpleCache方法");
        return "test";
    }

    /**
     * 如果缓存中存在对应的ID,则直接从缓存中获取结果,避免重复执行耗时操作。
     * 如果缓存中不存在,则执行方法体中的逻辑,将结果存入缓存并返回。
     * 方法执行过程中,通过Thread.sleep模拟了一个耗时操作。
     */
    @GetMapping("/{id}")
    @Cacheable(cacheNames = "cache:cacheByKey", key = "#id")
    public Integer cacheByKey(@PathVariable("id") Integer id) throws InterruptedException {
        Thread.sleep(5000);
        log.info("执行了cacheByKey方法" + id);
        return id;
    }

    /**
     * <p>该方法使用@Caching注解集成了多个缓存策略:</p>
     * <ul>
     *     <li>
     *         当方法返回值为null时(即缓存穿透情况),使用名为"cacheNullManager"的CacheManager进行缓存处理,
     *         缓存名称为"cache2:cacheByKey",缓存键为传入的用户ID,并设置缓存过期时间为10分钟。
     *         这通过@Cacheable注解的cacheManager属性指定缓存管理器,unless属性设置缓存条件(当结果为null时缓存)。
     *     </li>
     *     <li>
     *         当方法返回值不为null时,使用默认的CacheManager进行缓存处理,
     *         缓存名称和键的设置与上述相同,但此时缓存管理器为默认配置。
     *         这通过另一个@Cacheable注解实现,其unless属性设置为当结果为null时不缓存。
     *     </li>
     * </ul>
     *
     * <p>在方法执行过程中,通过Thread.sleep模拟了一个耗时操作。</p>
     */
    @Caching(
            cacheable = {
                    //result为null时,属于缓存穿透情况,使用cacheNullManager缓存管理器进行缓存,并且设置过期时间为10分钟。
                    @Cacheable(cacheNames = "cache2:cacheByKey", key = "#id", unless = "#result != null", cacheManager = "cacheNullManager"),
                    //result不为null时,使用默认缓存管理器进行缓存。
                    @Cacheable(cacheNames = "cache2:cacheByKey", key = "#id", unless = "#result == null")
            }
    )
    @GetMapping("/cacheMore/{id}")
    public User cacheMore(@PathVariable("id") Integer id) throws InterruptedException {
        Thread.sleep(5000);
        if (id > 100) {
            return null;
        } else {
            return new User(id, "zhangsan");
        }
    }

    @PostMapping("/cacheByUser")
    @Cacheable(cacheNames = "cache2:cacheByUser", key = "#user.id")
    public User cacheByUser(@RequestBody User user) throws InterruptedException {
        Thread.sleep(5000);
        log.info("执行了cacheByUser方法" + user.getId());
        return user;
    }

    @PostMapping("/cacheByIdAndName")
    @Cacheable(cacheNames = "cache2:cacheByUser", key = "#user.id")
    public User cacheByIdAndName(@RequestBody User user) throws InterruptedException {
        Thread.sleep(5000);
        log.info("执行了cacheByUser方法" + user.getId());
        return user;
    }

    /**
     * 根据用户ID大于100的条件进行缓存处理。
     *
     * @param user 用户对象,包含用户ID等信息。
     * @return 返回传入的用户对象。
     * @throws InterruptedException 如果线程被中断,则抛出此异常。
     *
     *                              通过@Cacheable注解实现了缓存功能,当请求的用户ID大于100时,会触发缓存机制。
     *                              缓存的名称设置为"cache2:cacheByUser",缓存的键为传入的用户对象的ID。
     *                              如果缓存中已存在对应的用户数据,则直接从缓存中获取并返回,避免重复执行耗时操作。
     *                              如果缓存中不存在,则执行方法体中的逻辑,将结果存入缓存并返回。
     *                              在方法执行过程中,通过Thread.sleep模拟了一个耗时操作。
     */
    @PostMapping("/cacheByUserIdGt100")
    @Cacheable(cacheNames = "cache2:cacheByUser", key = "#user.id", condition = "#user.id > 100")
    public User cacheByUserIdGt100(@RequestBody User user) throws InterruptedException {
        Thread.sleep(5000);
        log.info("执行了cacheByUser方法" + user.getId());
        return user;
    }

    /**
     * 更新用户信息。
     * <p>
     * 使用@CachePut注解将更新后的用户信息存入缓存中。
     * 缓存的名称设置为"cache2:cacheByUser",缓存的键为传入的User对象的ID。
     * 如果缓存中已存在对应的用户数据,则更新缓存中的值;如果不存在,则创建新的缓存条目。
     * 在方法执行过程中,通过Thread.sleep模拟了一个耗时操作。
     */
    @PostMapping("/updateUser")
    @CachePut(cacheNames = "cache2:cacheByUser", key = "#user.id")
    public User updateUser(@RequestBody User user) throws InterruptedException {
        Thread.sleep(5000);
        log.info("执行了saveUser方法" + user.getId());
        return user;
    }

    /**
     * 删除指定ID的用户,并从缓存中移除对应的数据。
     * <p>
     * 使用@CacheEvict注解用于从缓存中移除指定ID的用户数据。
     * 缓存的名称设置为"cache2:cacheByUser",缓存的键为传入的用户ID。
     * 在执行删除操作前,方法通过Thread.sleep模拟了一个耗时操作。
     */
    @DeleteMapping("/{id}")
    @CacheEvict(cacheNames = "cache2:cacheByUser", key = "#id")
    public void deleteUser(@PathVariable("id") Integer id) throws InterruptedException {
        Thread.sleep(10000);
        log.info("执行了deleteUser方法" + id);
    }
}

模拟了多种缓存使用的方式

  • updateUser使用@CachePut对数据进行缓存或更新。
  • deleteUser使用@CacheEvict删除缓存。
  • cacheMore根据条件选择不同的缓存管理器进行缓存数据。

简单附几张测试截图吧

第一次查询,没有缓存截图:

image-20250108110651822

后续查询走缓存的截图

image-20250108110707747

redis缓存数据格式:

image-20250108110505496

redis缓存数据详情:

image-20250108110447105

4. SpEL在Spring Cache中的应用

4.1 SpEL概述

SpEL是Spring框架提供的一种功能强大的表达式语言,它能够在运行时查询和操作对象图。SpEL的语法简洁,支持方法调用、字符串模板、集合操作、逻辑运算等复杂功能,使得在Spring配置和代码中能够更轻松地处理复杂的逻辑和数据结构。

4.2 SpEL应用

  1. 动态生成缓存键

    • 在Spring Cache中,缓存键(Key)用于在缓存中唯一标识数据。通过使用SpEL表达式,可以根据方法参数、返回值等动态生成缓存键。
    • 例如,在@Cacheable注解中,可以使用key属性配合SpEL表达式来指定缓存键的生成规则。
  2. 条件缓存

    • Spring Cache允许通过condition属性来指定缓存的条件。当条件满足时,才会执行缓存操作(如缓存数据或移除缓存)。
  3. 除非条件

    • unless属性与condition属性类似,但它用于指定不执行缓存操作的条件。
    • 当unless条件满足时,即使方法被调用,其结果也不会被缓存。
    • unless属性同样支持SpEL表达式。

4.3 SpEL表达式在Spring Cache中的常用变量

  1. #参数名

    • 表示方法参数。可以通过参数名来引用方法参数的值。
    • 例如,#param1表示第一个参数的值。
  2. #result

    • 表示方法的返回值。在@CachePut和@CacheEvict注解中,可以使用#result来引用方法的返回值。
  3. #root

    • 表示缓存表达式根对象(CacheExpressionRootObject)。它提供了对缓存操作上下文的访问。
    • 通过#root,可以获取到缓存的详细信息,如缓存名称、方法参数等。

注意:

condition属性在Spring Cache中用于在方法执行前判断是否执行缓存操作,并且不能引用方法的返回值;而unless属性则用于在方法执行后根据返回值或其他条件来决定是否缓存数据。

5. 工作原理

Spring Cache是基于AOP原理,对添加注解@Cacheable的类生成代理对象,在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用源方法获取数据返回,并缓存起来,下边跟踪Spring Cache的切面类CacheAspectSupport.java中的private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts)方法。

@Nullable
    private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
        if (contexts.isSynchronized()) {
            CacheOperationContext context = (CacheOperationContext)contexts.get(CacheableOperation.class).iterator().next();
            if (!this.isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
                return this.invokeOperation(invoker);
            }

            Object key = this.generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
            Cache cache = (Cache)context.getCaches().iterator().next();

            try {
                return this.wrapCacheValue(method, this.handleSynchronizedGet(invoker, key, cache));
            } catch (Cache.ValueRetrievalException var10) {
                Cache.ValueRetrievalException ex = var10;
                ReflectionUtils.rethrowRuntimeException(ex.getCause());
            }
        }

        this.processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT);
        Cache.ValueWrapper cacheHit = this.findCachedItem(contexts.get(CacheableOperation.class));
        List<CachePutRequest> cachePutRequests = new ArrayList();
        if (cacheHit == null) {
            this.collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
        }

        Object cacheValue;
        Object returnValue;
        if (cacheHit != null && !this.hasCachePut(contexts)) {//如果缓存有,则从缓存取
            cacheValue = cacheHit.get();
            returnValue = this.wrapCacheValue(method, cacheValue);
        } else {//缓存没有,执行原始方法
            returnValue = this.invokeOperation(invoker);
            cacheValue = this.unwrapReturnValue(returnValue);//再存缓存
        }

        this.collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
        Iterator var8 = cachePutRequests.iterator();

        while(var8.hasNext()) {
            CachePutRequest cachePutRequest = (CachePutRequest)var8.next();
            cachePutRequest.apply(cacheValue);
        }

        this.processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
        return returnValue;
    }

6. 本文源码

使用Redis的过程中还会有很多问题,比如缓存数据一致性,缓存数据持久化,内存淘汰机制,缓存雪崩等等等,在面试的时候也经常会用到,博主整理了一份Redis常见的面试,感兴趣的朋友可以看下:

【面试1v1实景模拟】Redis面试官会怎么提问?

本文源码:https://github.com/hack-feng/maple-product/

其中maple-redis模块即为本文的Demo源码。需要的朋友可以看下。

感兴趣的朋友可以帮忙点个star⭐⭐⭐⭐⭐后续会有更多Java相关的集成Demo,让我来做你的百宝袋吧。

🐾我是笑小枫,全网皆可搜的【笑小枫】

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

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

相关文章

MySQL事件功能简介

MySQL 的事件调度器&#xff08;Event Scheduler&#xff09;提供了一种便捷的方法来定时执行 SQL 语句&#xff0c;从而实现数据维护、报告生成等自动化操作。本文将详细介绍 MySQL 的事件功能&#xff0c;并说明如何使用 Navicat 管理这些事件。 1. 什么是 MySQL 事件调度器…

高光谱相机的特点

光谱特性 高光谱分辨率&#xff1a;能将光谱范围分割成极窄的波段&#xff0c;光谱分辨率通常达到纳米级甚至亚纳米级&#xff0c;可精确捕捉到不同物质在细微光谱差异上的特征&#xff0c;比如可以区分不同种类的植被因叶绿素含量等差异而在光谱上的细微变化。 多波段探测&a…

备考蓝桥杯:数据结构概念浅谈

目录 1数据结构的概念 什么是数据结构: 为什么要有数据结构 2.数据结构的三个组成要素 1.逻辑结构 2.存储结构 3.数据运算 3。算法好坏的度量&#xff08;时间复杂度和空间复杂度&#xff09; 时间复杂度计算 最优和平均和最差时间复杂度 计算时间复杂度例子 空间复…

闲谭SpringBoot--ShardingSphere分库分表探究

文章目录 1. 背景2. 创建数据库3. 修改yml配置文件4. 分片算法类5. 测试6 小结 1. 背景 接上文&#xff0c;我们对日志表&#xff0c;进行了按月的分表&#xff0c;这样每个月几百万条数据量还是扛得住的。 但是如果数据再多呢&#xff0c;除了提高硬件性能&#xff0c;还有一…

基于伪分布式模式部署Hadoop集群

1.上传Hadoop安装包 在/export/software目录下使用rz命令上传Hadoop安装包 2.创建目录 在/export/servers目录下创建wfb-hadoop目录&#xff0c;用于存放Hadoop的安装目录&#xff0c;命令如下&#xff1a; mkdir -p /export/servers/wfb-hadoop 3.安装Hadoop 1)将Hadoop安…

Android车载音频系统目录

目录 第一章 1.1 Android Automotive&#xff08;一&#xff09; 1.2 Android Automotive&#xff08;二&#xff09; 1.3 Android Automotive&#xff08;三&#xff09; 第二章 2.1 Android车载音频系统概览 2.2 车载音频焦点 2.3 车载音频配置 2.4 Audio control HAL…

怎么管理电脑usb接口,分享四种USB端口管理方法

怎么管理电脑usb接口&#xff0c;分享四种USB端口管理方法 USB接口作为电脑重要的外部接口&#xff0c;方便了数据传输和设备连接。 然而&#xff0c;不加管理的USB接口也可能带来安全隐患&#xff0c;例如数据泄露、病毒传播等。 因此&#xff0c;有效管理电脑USB接口至关重…

React+redux项目搭建流程

1.创建项目 create-react-app my-project --template typescript // 创建项目并使用typescript2.去除掉没用的文件夹&#xff0c;只保留部分有用的文件 3.项目配置&#xff1a; 配置项目的icon 配置项目的标题 配置项目的别名等&#xff08;craco.config.ts&…

conda+jupyter+pycharm:如何在Windows conda环境下运行jupyter并使用浏览器或者pycharm运行.ipynb

1 安装conda 2 conda环境下安装jupyter pip install jupyter3 设置jupyter配置文件 1&#xff09;创建 jupyter_notebook_config.py文件 jupyter notebook --generate-config 2&#xff09;设置密码 3&#xff09;设置参数 直接将以下参数修改为自己的配置后复制到配置文件…

微信小程序获取图片使用session(上篇)

概述&#xff1a; 我们开发微信小程序&#xff0c;从后台获取图片现实的时候&#xff0c;通常采用http get的方式&#xff0c;例如以下代码 <image class"user_logo" src"{{logoUrl}}"></image>变量logoUrl为ur图片l的请求地址 但是对于很多…

【江协STM32】9-1/2/3 USART串口协议、USART外设、串口发送串口发送+接收

1. 通信接口 通信的目的&#xff1a;将一个设备的数据传送到另一个设备&#xff0c;扩展硬件系统通信协议&#xff1a;制定通信的规则&#xff0c;通信双方按照协议规则进行数据收发全双工&#xff1a;指通信双方能够同时进行双向通信。发送线路和接收线路互不影响&#xff0c…

第一 二章 小车硬件介绍-(全网最详细)基于STM32智能小车-蓝牙遥控、避障、循迹、跟随、PID速度控制、视觉循迹、openmv与STM32通信、openmv图像处理、smt32f103c8t6

第一篇-STM32智能小车硬件介绍 后续章节也放这里 持续更新中&#xff0c;视频发布在小B站 里面。这边也会更新。 B站视频合集: STM32智能小车V3-STM32入门教程-openmv与STM32循迹小车-stm32f103c8t6-电赛 嵌入式学习 PID控制算法 编码器电机 跟随 小B站链接:https://www.bilib…

【网络】电路交换(Circuit Switching)、报文交换(Message Switching)和分组交换(Packet Switching)

电路交换&#xff08;Circuit Switching&#xff09;&#xff1a;一条专用的通信线路&#xff08;或电路&#xff09;&#xff08; 电话专用线路&#xff0c;好处&#xff1a;专用稳定&#xff0c;有没有数据都被占用&#xff0c;坏处&#xff1a;容易浪费&#xff09; 报文交换…

Pixel 6a手机提示无法连接移动网络,打电话失败!

1、开启VoLTE 2、如果没有&#xff0c;下载shizuku和PixelIMS应用。 shizuke Releases RikkaApps/Shizuku GitHub PixellMS Release v1.2.8 kyujin-cho/pixel-volte-patch GitHub 3、安装shizuke启动&#xff0c;开通root可以直接点击下面的启动&#xff0c;如果没有就…

游戏关卡设计的常用模式

游戏关卡分为很多种&#xff0c;但常用的有固定套路&#xff0c;分为若干种类型。 关卡是主角与怪物、敌方战斗的场所&#xff0c;包括装饰物、通道。 单人游戏的关卡较小&#xff0c;偏线性&#xff1b; 联机/MMO的关卡较大&#xff0c;通道多&#xff0c;自由度高&#xf…

DC/AC并网逆变器模型与仿真MATLAB

DC/AC并网逆变器是一种将直流电&#xff08;DC&#xff09;转化为交流电&#xff08;AC&#xff09;&#xff0c;并将其与电网并联的设备。它的核心功能是实现直流电源&#xff08;如光伏电池板或储能电池&#xff09;与电网的有效连接&#xff0c;同时保证输出电能质量满足电网…

作业:IO:day2

题目一 第一步&#xff1a;创建一个 struct Student 类型的数组 arr[3],初始化该数组中3个学生的属性 第二步&#xff1a;编写一个叫做save的函数&#xff0c;功能为 将数组arr中的3个学生的所有信息&#xff0c;保存到文件中去&#xff0c;使用fread实现fwrite 第三步&#xf…

环动科技平均售价波动下滑:大客户依赖明显,应收账款周转率骤降

《港湾商业观察》施子夫 2024年12月18日&#xff0c;浙江环动机器人关节科技股份有限公司&#xff08;以下简称&#xff0c;环动科技&#xff09;的上市审核状态变更为“已问询”&#xff0c;公司在11月25日科创板IPO获上交所受理&#xff0c;独家保荐机构为广发证券。 此次环…

【数据可视化-11】全国大学数据可视化分析

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

SAP 02-AMDP Functions for CDS Table Functions

1. 创建一个Core Data Service Table Functions 新建 Core Data Service Table Function 定义CDS Table Functions EndUserText.label: a simple AMDP for CDS Table Functions ClientDependent: true //打开 Open SQL 的自动客户端处理 defin…