Spring cache整合Redis详解 动态设置失效时间

news2024/10/5 19:17:20

文章目录

      • 1.spring cache简介
      • 2.spring cache集成redis
      • 3.spring cache与redisTemple统一格式
      • 4.SpEL标签
      • 5.Cacheable注解实现
      • 6.CachePut注解实现
      • 7.CacheEvict注解实现
      • 8.Caching注解实现
      • 9.自定义key生成器KeyGenerator
      • 10.自定义前缀CacheKeyPrefix
      • 11.多个CacheManager实现不同失效时间
      • 12.自定义CacheResolver动态设置失效时间
      • 13.完整核心代码

1.spring cache简介

​ Spring Cache是Spring框架提供的对缓存使用的抽象类,Spring从3.1版本开始提供Cache和CacheManager来统一管理不同的缓存插件,它使用注解的方式实现,避免了代码的侵入性,使业务代码和操作缓存的代码分离。支持集成的插件从CacheType中可以看到:

public enum CacheType {

	GENERIC,
	JCACHE,
	EHCACHE,
	HAZELCAST,
	INFINISPAN,
	COUCHBASE,
	REDIS,
	CAFFEINE,
	SIMPLE,
	NONE

}

2.spring cache集成redis

​ 项目中使用redis比较多,此处实现集成redis的过程。

(1)引入依赖

​ 在pom.xml中引入需要的依赖,此处使用spring cloud项目实现,版本号在父工程中定义,子工程中引入具体需要的依赖。

​ 父工程pom.xml引入依赖,指定版本号:

   <!--指定版本号-->
   <properties>
        <spring.boot.version>2.3.3.RELEASE</spring.boot.version>
        <lombok.version>1.18.20</lombok.version>
    </properties>
    
   <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <!--使用redis时需要此jar包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
   </dependencies>

   <!--使用dependencyManagement来管理版本号,子项目依赖parent后,不需要添加版本号,达到版本号的统一控制-->
   <dependencyManagement>
        <dependencies>
            <!--spring boot依赖jar-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
   </dependencyManagement>

​ 子工程pom.xml引入需要的依赖:

    <!--继承了父项目,不需要添加版本号-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--spring cache-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <!--redis依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

(2)配置连接信息

​ 在yml或者properties配置文件中配置上服务的端口信息、连接redis信息、spring cache配置信息。application.yml配置:

#配置端口
server:
  port: 8090

#配置连接redis的信息
spring:
  redis:
    database: 1
    host: 127.0.0.1
    port: 6379
    password:
    timeout: 3000
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        max-wait: -1
        min-idle: 0

  #配置spring cache
  cache:
    #配置缓存组件类型,分为generic、jcache、ehcache、hazelcast、infinispan、couchbase、redis、caffeine、simple、none
    type: redis
    #组件类型选择redis后,对操作redis的一些配置信息
    redis:
      #是否允许存空值,防止缓存穿透
      cache-null-values: true
      #缓存失效时间
      time-to-live: -1
      #存到redis中是否使用key前缀
      use-key-prefix: true
      #key前缀
      key-prefix: abc

(3)配置spring cache配置类

​ 创建一个配置类,用于配置CacheManager管理器,以及添加到redis数据value的序列化方式、对象序列化的格式处理、设置过期时间、是否允许前缀、前缀的拼接等。使用@EnableCaching注解标识开启spring cache,可以在配置类中声明也可以在Application启动类中声明。

//声明配置类
@Configuration
//开启spring cache
@EnableCaching
//添加缓存配置类,yml或者properties中配置的cache相关信息,使用CacheProperties接收,从这里引入,可以直接使用里面的字段值
@EnableConfigurationProperties(CacheProperties.class)
public class CacheConfig{

    //创建缓存管理类,@Primary标识当有多个CacheManager管理器时,默认以这个为主
    //redisConnectionFactory为连接redis的工厂,在子类中已经创建并且使用@Bean标识
    //CacheProperties为配置的spring cache相关信息,类被@ConfigurationProperties修饰
    @Bean
    @Primary
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,CacheProperties cahceProperties){
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration(cahceProperties,null))
                .build();
    }

    //设置rediscacheConfiguration配置类,根据配置的失效时间等属性进行配置
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cahceProperties,Long ttl) {
        //使用Jackson2JsonRedisSerializer的方式类序列化值
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        //添加时间日期格式的处理
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(dtf));
        javaTimeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(dtf));
        om.registerModule(javaTimeModule);

        jackson2JsonRedisSerializer.setObjectMapper(om);
        //获取RedisCacheConfiguration,对它进行新值的设置,默认序列化值使用SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader))
        //这里设置序列化值的方式与redisTemple保持一致,也必须保持一致,否则有些使用spring cache存再使用redisTemple来取的时候,格式就会存在问题
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(
                        jackson2JsonRedisSerializer));

        //获取配置的信息
        CacheProperties.Redis redisProperties = cahceProperties.getRedis();
        //设置过期时间
        if(null == ttl) {//传递的参数为空,则使用配置文件中配置的过期时间
            if (null != redisProperties.getTimeToLive()) {
                config = config.entryTtl(redisProperties.getTimeToLive());
            }
        } else {
            config = config.entryTtl(Duration.ofMillis(ttl));
        }

        //是否允许设置为null值
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        //是否设置前缀
        if (redisProperties.isUseKeyPrefix()) {
            //设置前缀
            if (null != redisProperties.getKeyPrefix()) {
                config = config.computePrefixWith(CacheKeyPrefix.prefixed(redisProperties.getKeyPrefix()));//默认的前缀方式
            }
        } else {
            config = config.disableKeyPrefix();
        }
        return config;
    }
 }

3.spring cache与redisTemple统一格式

​ 项目集成了spring cache,操作缓存我们都使用spring cache注解的方式,但是有些场景下,我们没法使用spring cache。例如新增一条记录的时候,接口的返回值是执行结果,这个时候就没法使用spring cache来操作缓存,会选择redisTemple操作。这样就会存在redisTemple存spring cache取,或者spring cache存redisTemple取的情况,这个时候就要求两种方式存的格式统一,否则取的时候格式不统一没法转成需要的对象实体。spring cache对一个结果对象实体的存相当于redisTemple这样的方式存:redisTemplate.opsForValue().set(“user:”+id,user)。

​ 创建redis配置类,设置对象序列化的方式与spring cache一致,这样redisTemple和spring cache 交叉存取就没有格式问题:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //添加时间日期格式的处理
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(dtf));
        javaTimeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(dtf));
        om.registerModule(javaTimeModule);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

4.SpEL标签

​ spring cache提供了一些供我们使用的SpEL上下文数据,可以根据SpEL标签获取到参数值,进行一些动态数据的组装,例如获取到参数id的值,作为redis的key。

名称位置描述示例
methodNameroot对象当前被访问的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#root.caches[0].name
ArgumentName执行上下文当前被调用方法的参数,例如findUser(User user),可以通过#user.id获得参数的值#user.id
result执行上下文方法执行后的返回值(仅当方法执行后的判断有效,例如unless=#result==null#result

5.Cacheable注解实现

​ 这个注解一般用在查询方法上,表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有此方法对应key的值,如果有就直接返回,不调用方法;如果没有,则调用方法,并把结果缓存起来。支持配置的属性:

属性/方法名说明
value缓存名,必填,它指定了缓存存放在哪块命名空间下
cacheNames与value等价,两者二选一即可
key可选属性,可以使用SpEL标签定义缓存的key
keyGeneratorkey的生成器,key/keyGenerator二选一即可
cacheManager指定缓存管理器,可以定义多个缓存管理器,每个设置不同的特性,例如过期时间
cacheResolver指定缓存解析器,可以自定义
condition条件符合则缓存
unless条件符合则不缓存
sync是否使用异步模式,默认为false

​ 为了处理查询缓存失效的方案:

①缓存穿透:spring cache配置文件的属性cache-null-values:true,允许写入空值。

②缓存击穿:@Cacheable(sync=true),查询加锁。

③缓存雪崩:每个缓存配置不同的过期时间,time-to-live:xxx。

案例:

    @PostMapping(value="getUserById1")
    @Cacheable(cacheNames = "aaaa",key = "#root.args[0]")
    public User getUserById1(Integer id){
        return olapService.getUserById(id);
    }

    @PostMapping(value="getUserById6")
    //unless:SpEL表达式 不满足条件才缓存数据
    @Cacheable(cacheNames = "ffff", key = "#root.args[0]",unless ="#result==null")
    public User getUserById6(Integer id){
        return olapService.getUserById(id);
    }

6.CachePut注解实现

​ 使用该注解标识的方法,每次都会执行,并将返回值存入指定的缓存中,若是之前已经有此key对应的value,则再次执行相当于覆盖,一般用在新增、修改方法上。支持配置的属性:

属性/方法名说明
value缓存名,必填,它指定了缓存存放在哪块命名空间下
cacheNames与value等价,两者二选一即可
key可选属性,可以使用SpEL标签定义缓存的key
keyGeneratorkey的生成器,key/keyGenerator二选一即可
cacheManager指定缓存管理器,可以定义多个缓存管理器,每个设置不同的特性,例如过期时间
cacheResolver指定缓存解析器,可以自定义
condition条件符合则缓存
unless条件符合则不缓存

案例:

    @PostMapping(value="insertUser")
    //CachePut每次都会执行,并将结果存入缓存中,常用于更新、插入中
    @CachePut(cacheNames = "gggg", key = "#user.id",unless ="#result==null")
    public User insertUser(@RequestBody User user){
        return olapService.insertUser(user);
    }

7.CacheEvict注解实现

​ 使用该注解的方法,会清空指定的缓存,一般用在更新、删除方法上。支持配置的属性:

属性/方法名说明
value缓存名,必填,它指定了缓存存放在哪块命名空间下
cacheNames与value等价,两者二选一即可
key可选属性,可以使用SpEL标签定义缓存的key
keyGeneratorkey的生成器,key/keyGenerator二选一即可
cacheManager指定缓存管理器,可以定义多个缓存管理器,每个设置不同的特性,例如过期时间
cacheResolver指定缓存解析器,可以自定义
condition条件符合则缓存
allEntries是否清空所用的缓存,默认为false,如果指定为true,则方法调用后将清空所有命名空间为value或cacheNames配置值的数据
beforeInvocation是否在方法执行前就清空,默认为false,如果指定为true,则在方法执行前就会清空缓存

案例:

    @PostMapping(value="deleteUser")
    //CacheEvict清空指定的缓存
    @CacheEvict(cacheNames = "gggg", key = "#root.args[0]")
    public boolean deleteUser(Integer id){
        return olapService.deleteUser(id);
    }

    @PostMapping(value="deleteUser1")
    //CacheEvict清空所有cacheNames命名空间下的数据
    @CacheEvict(cacheNames = "gggg", key = "#root.args[0]",allEntries = true)
    public boolean deleteUser1(Integer id){
        return olapService.deleteUser(id);
    }

8.Caching注解实现

​ 该注解可以实现同一个方法上同时使用多种注解,从Caching的源码可以看到支持Cacheable、CachePut、CacheEvict数组。Caching源码:

public @interface Caching {

	Cacheable[] cacheable() default {};

	CachePut[] put() default {};

	CacheEvict[] evict() default {};

}

案例:

   @PostMapping(value="deleteUserCaching")
    //Caching可以同时使用多个缓存,参数为cacheable、put、evict
    @Caching(
        cacheable = {
            @Cacheable(cacheNames = "hhhh",key = "#root.args[0]"),
            @Cacheable(cacheNames = "iiii",key = "#root.args[0]"),
        },
        put = {
            @CachePut(cacheNames = "jjjj", key = "#root.args[0]"),
            @CachePut(cacheNames = "kkkk", key = "#root.args[0]")
        },
        evict = {
            @CacheEvict(cacheNames= "eeee",key = "#root.args[0]"),
            @CacheEvict(cacheNames= "ffff",key = "#root.args[0]"),
        }
    )
    public boolean deleteUserCaching(Integer id){
        return olapService.deleteUser(id);
    }

9.自定义key生成器KeyGenerator

​ 缓存的key可以通过属性key进行配置,也可以使用key生成器进行生成,这样就不用每个key都去具体指定,key和key生成器二选一就行。key生成器使用@Bean修饰,当程序启动的时候,会加载到spring容器中进行管理,需要的时候直接使用即可。配置key生成器,可以放在CacheConfig.java配置类中:

    @Bean
    public KeyGenerator customKeyGenerator(){
        return new KeyGenerator() {
            //根据target类、调用的方法method、传递的参数params组织spring cache的key
            @Override
            public Object generate(Object target, Method method, Object... params) {
                String key = target.getClass().getSimpleName() + "_" + method.getName() + "_" + StringUtils.arrayToDelimitedString(params, "_");
                return key;
            }
        };
    }

使用时通过属性keyGenerator指定key的生成器。

案例:

    @PostMapping(value="getUserById5")
    //存到redis的key使用key生成器的方式生成,配置属性key和keyGenerator二选一就行
    @Cacheable(cacheNames = "eeee", keyGenerator = "customKeyGenerator")
    public User getUserById5(Integer id,String name){
        return olapService.getUserById(id);
    }

redis存放情况:
在这里插入图片描述

10.自定义前缀CacheKeyPrefix

​ key是否需要加统一的前缀、以及设置统一的前缀字符都可以在yml或properties配置文件中配置,spring.cache.redis.use-key-prefix配置是否使用前缀,值为true则开启;spring.cache.redis.key-prefix配置前缀字符。默认的前缀配置格式为key-prefix+cacheNames的值+::,从CacheKeyPrefix的源码可以看出:

 static CacheKeyPrefix prefixed(String prefix) {
        Assert.notNull(prefix, "Prefix must not be null!");
        return (name) -> {
            return prefix + name + "::";
        };
    }

默认key的格式:
在这里插入图片描述
觉得这样的::连接符不美观的话,可以自定义key的格式,在RedisCacheConfiguration配置中,可以重新配置key的格式。

 //添加上前缀,也可以自定义一个方式,默认是keyPrefix::的方式
 config = config.computePrefixWith(bulidPrefix(redisProperties.getKeyPrefix())); 

 //重新构建key的连接格式
 private CacheKeyPrefix bulidPrefix(String keyPrefix){
        return (name) -> {
            StringBuffer str = new StringBuffer();
            str.append(keyPrefix);
            str.append(":");
            str.append(name);
            str.append(":");
            return str.toString();
        };
 }

11.多个CacheManager实现不同失效时间

​ 有的时候,我们想为不同的key设置不同的失效时间,例如设置失效时间为一分钟、一个小时、一天等。为了达到这个需求,可以定义不同的CacheManager管理器,当配置缓存的时候,用属性cacheManager来指定选择的CacheManager。

​ 在CacheConfig.java配置类中配置需要的管理器,使用 @Primary注解标识哪个是默认的管理器,这样在不指定CacheManager的时候,会使用默认的管理器。配置不同管理器的实现:

    //创建缓存管理类,@Primary标识当有多个CacheManager管理器时,默认以这个为主
    //redisConnectionFactory为连接redis的工厂,在子类中已经创建并且使用@Bean标识
    //CacheProperties为配置的spring cache相关信息,类被@ConfigurationProperties修饰
    @Bean
    @Primary
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,CacheProperties cahceProperties){
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration(cahceProperties,null))
                .build();
    }

    //创建其它缓存管理类,使用的时候,根据cacheManager来指定使用哪个cache管理器,默认使用被 @Primary修饰的。例如:  @Cacheable(cacheNames = "xxxx",key = "#root.args[0]",cacheManager = "cacheManagerOneMinute")
    //这样就可以根据业务需要配置不同的cache管理器,一般用于设置redis不同的失效时间,此为设置失效时间为1分钟
    @Bean
    public CacheManager cacheManagerOneMinute(RedisConnectionFactory redisConnectionFactory,CacheProperties cahceProperties){
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration(cahceProperties,60000L))
                .build();
    }

    //这样就可以根据业务需要配置不同的cache管理器,一般用于设置redis不同的失效时间,此为设置失效时间为1小时
    @Bean
    public CacheManager cacheManagerOneHour(RedisConnectionFactory redisConnectionFactory,CacheProperties cahceProperties){
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration(cahceProperties,3600000L))
                .build();
    }

案例:

    @PostMapping(value="getUserById2")
    //指定使用哪种cache管理器,不指定使用默认的
    @Cacheable(cacheNames = "bbbb",key = "#root.args[0]",cacheManager = "cacheManagerOneHour")
    public User getUserById2(Integer id){
        return olapService.getUserById(id);
    }

12.自定义CacheResolver动态设置失效时间

​ 不同的失效时间可以通过配置不同的CacheManager实现,但是不灵活,每次有一个新的失效时间,都得创建一个新的CacheManager。若是每次操作缓存的时候都可以动态的配置失效时间,或者配置今天内有效,这样就特别方便了。spring cache是基于aop实现的,我们也可以基于这个特性来动态设置失效时间。

​ 自定义一个注解,支持配置缓存失效时间、时间单位、是否今天失效(有效时间从操作缓存这一刻到凌晨十二点)。自定义注解CacheExpire:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * cache注解类,可以配置cache失效时间
 */
//此注解作用于方法上
@Target(ElementType.METHOD)
//此注解保留到编译成class文件,加载到jvm中也依然存在
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheExpire {

    /**
     * 失效时间,默认60秒
     */
    public long ttl() default 60L;

    /**
     * 失效单位,默认秒
     */
    public TimeUnit unit() default TimeUnit.SECONDS;

    /**
     * 今天有效,失效时间是从创建这一刻起,到晚上凌晨12点整,是一个动态的时间
     */
    public boolean today() default false;
}

​ 重写CacheResolver缓存处理器的方法,在处理缓存之前,使用反射机制动态的修改RedisCacheConfiguration里面的失效时间,这样就可以随意配置失效时间。新建一个处理器RedisExpireCacheResolver,让其继承SimpleCacheResolver,重写resolveCaches方法,通过拦截执行的方法检查是否配置了自定义注解CacheExpire,配置了CacheExpire注解,则从对应字段取到失效时间,配置了今天失效(today为true),则计算当前这一刻到12点的时长,重新设置失效时间。RedisExpireCacheResolver.java实现代码:

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.cache.interceptor.SimpleCacheResolver;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.*;

/**
 * 使用继承的方式对cache处理器进行扩展
 */
public class RedisExpireCacheResolver extends SimpleCacheResolver {

    public RedisExpireCacheResolver(CacheManager cacheManager) {
        super(cacheManager);
    }

    //重写处理cache的方法
    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        //直接参考父父类AbstractCacheResolver的resolveCaches方法
        //获取当前注解中的缓存名,通过父类进行获取
        Collection<String> cacheNames = getCacheNames(context);
        if (cacheNames == null) {
            return Collections.EMPTY_LIST;
        } else {
            Collection<Cache> result = new ArrayList(cacheNames.size());
            Iterator cacheIterator = cacheNames.iterator();
            while (cacheIterator.hasNext()) {
                String cacheName = (String) cacheIterator.next();
                //通过缓存名从缓存管理器中获取到缓存对象
                Cache cache = this.getCacheManager().getCache(cacheName);
                if (cache == null) {
                    throw new IllegalArgumentException("Cannot find cache named '" + cacheName + "' for " + context.getOperation());
                }
                //跟AbstractCacheResolver的resolveCaches方法比较,只有这里新增解析注解,反射替换RedisCacheConfiguration的处理
                this.parseCacheExpire(cache, context);

                result.add(cache);
            }
            return result;
        }
    }

    /**
     * 通过反射替换cache中的RedisCacheConfiguration类已经设置好的过期时间等的值
     * 解析自定义注解CacheExpire,从注解中获取设置的过期时间,给RedisCacheConfiguration重新赋值
     */
    private void parseCacheExpire(Cache cache, CacheOperationInvocationContext<?> context) {
        Method method = context.getMethod();
        //判断方法是否包含过期时间注解
        if (method.isAnnotationPresent(CacheExpire.class)) {  //包含CacheExpire注解再处理
            //获取到注解
            CacheExpire cacheExpire = method.getAnnotation(CacheExpire.class);
            Duration duration = null;
            //判断注解是否配置的过期时间为今天
            if(cacheExpire.today()){
                //当配置了过期时间为今天,则计算从这一刻到凌晨12点还有多少时间
                duration = Duration.ofSeconds(getSecondsNextEarlyMorning());
            } else {
                //过期时间为用户自己配置,则根据配置的来创建Duration
                duration = Duration.ofSeconds(cacheExpire.unit().toSeconds(cacheExpire.ttl()));
            }
            //转成RedisCache 这个时候cacheConfig是空的,也就让反射有了可乘之机
            RedisCache redisCache = (RedisCache) cache;
            //获取cache里面的RedisCacheConfiguration
            RedisCacheConfiguration cacheConfiguration = redisCache.getCacheConfiguration();
            //新生成一个configuration
            RedisCacheConfiguration cacheConfig = cacheConfiguration;
            //参数需要对应修改
            cacheConfig = cacheConfig.entryTtl(duration);
            //通过反射获取到类型为RedisCacheConfiguration的字段cacheConfig
            Field field = ReflectionUtils.findField(RedisCache.class, "cacheConfig", RedisCacheConfiguration.class);
            //设置可以访问被private修饰的字段值
            field.setAccessible(true);
            //重新设置替换RedisCacheConfiguration
            ReflectionUtils.setField(field, redisCache, cacheConfig);
        }
    }

    //获取当前时间到第二天凌晨的秒数,用于设置redis失效时间为当天
    private Long getSecondsNextEarlyMorning() {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_YEAR, 1);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return (cal.getTimeInMillis() - System.currentTimeMillis()) / 1000;
    }

}

我们需要把自定义的缓存处理类RedisExpireCacheResolver注册到spring容器中,可以在CacheConfig.java中使用@Bean进行标识:

 /**
     * 注册自定义的缓存处理类
     * cacheManager 为使用的缓存管理器
     * 使用CacheResolver来接收,它属于父接口类
     * 使用方式,指明处理器: @Cacheable(cacheNames = "yyyy",key = "#root.args[0]",cacheResolver = "redisExpireCacheResolver")
     */
    @Bean
    public CacheResolver redisExpireCacheResolver(CacheManager cacheManager){
        return new RedisExpireCacheResolver(cacheManager);
    }

在使用时,配置上@CacheExpire注解的值,通过属性cacheResolver来指定使用的缓存处理器。

案例:

    @PostMapping(value="getUserById3")
    //指定使用哪种cache处理器
    @Cacheable(cacheNames = "cccc",key = "#root.args[0]",cacheResolver = "redisExpireCacheResolver")
    //设置过期时间值、单位
    @CacheExpire(ttl = 20,unit = TimeUnit.SECONDS)
    public User getUserById3(Integer id){
        return olapService.getUserById(id);
    }

    @PostMapping(value="getUserById9")
    //指定使用哪种cache处理器
    @Cacheable(cacheNames = "cccc",key = "#root.args[0]",cacheResolver = "redisExpireCacheResolver")
    //设置今天有效
    @CacheExpire(today = true)
    public User getUserById9(Integer id){
        return olapService.getUserById(id);
    }

13.完整核心代码

CacheConfig.java:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheResolver;
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.CacheKeyPrefix;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

//声明配置类
@Configuration
//开启spring cache
@EnableCaching
//添加缓存配置类,yml或者properties中配置的cache相关信息,使用CacheProperties接收,从这里引入,可以直接使用里面的字段值
@EnableConfigurationProperties(CacheProperties.class)
public class CacheConfig{


    //创建缓存管理类,@Primary标识当有多个CacheManager管理器时,默认以这个为主
    //redisConnectionFactory为连接redis的工厂,在子类中已经创建并且使用@Bean标识
    //CacheProperties为配置的spring cache相关信息,类被@ConfigurationProperties修饰
    @Bean
    @Primary
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,CacheProperties cahceProperties){
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration(cahceProperties,null))
                .build();
    }

    //创建其它缓存管理类,使用的时候,根据cacheManager来指定使用哪个cache管理器,默认使用被 @Primary修饰的。例如:  @Cacheable(cacheNames = "xxxx",key = "#root.args[0]",cacheManager = "cacheManagerOneMinute")
    //这样就可以根据业务需要配置不同的cache管理器,一般用于设置redis不同的失效时间,此为设置失效时间为1分钟
    @Bean
    public CacheManager cacheManagerOneMinute(RedisConnectionFactory redisConnectionFactory,CacheProperties cahceProperties){
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration(cahceProperties,60000L))
                .build();
    }

    //这样就可以根据业务需要配置不同的cache管理器,一般用于设置redis不同的失效时间,此为设置失效时间为1小时
    @Bean
    public CacheManager cacheManagerOneHour(RedisConnectionFactory redisConnectionFactory,CacheProperties cahceProperties){
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration(cahceProperties,3600000L))
                .build();
    }

    //设置rediscacheConfiguration配置类,根据配置的失效时间等属性进行配置
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cahceProperties,Long ttl) {
        //使用Jackson2JsonRedisSerializer的方式类序列化值
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        //添加时间日期格式的处理
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(dtf));
        javaTimeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(dtf));
        om.registerModule(javaTimeModule);

        jackson2JsonRedisSerializer.setObjectMapper(om);
        //获取RedisCacheConfiguration,对它进行新值的设置,默认序列化值使用SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader))
        //这里设置序列化值的方式与redisTemple保持一致,也必须保持一致,否则有些使用spring cache存再使用redisTemple来取的时候,格式就会存在问题
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(
                        jackson2JsonRedisSerializer));

        //获取配置的信息
        CacheProperties.Redis redisProperties = cahceProperties.getRedis();
        //设置过期时间
        if(null == ttl) {//传递的参数为空,则使用配置文件配置的过期时间
            if (null != redisProperties.getTimeToLive()) {
                config = config.entryTtl(redisProperties.getTimeToLive());
            }
        } else {
            config = config.entryTtl(Duration.ofMillis(ttl));
        }

        //是否允许设置为null值
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        //是否设置前缀
        if (redisProperties.isUseKeyPrefix()) {
            //设置前缀
            if (null != redisProperties.getKeyPrefix()) {
                //添加上前缀,也可以自定义一个方式,默认是kerPrefix::的方式
                config = config.computePrefixWith(bulidPrefix(redisProperties.getKeyPrefix()));  //自定义前缀
                //config = config.computePrefixWith(CacheKeyPrefix.prefixed(redisProperties.getKeyPrefix()));//默认的前缀方式
            }
        } else {
            config = config.disableKeyPrefix();
        }
        return config;
    }

    /**
     * 自定义前缀
     */
    private CacheKeyPrefix bulidPrefix(String keyPrefix){
        return (name) -> {
            StringBuffer str = new StringBuffer();
            str.append(keyPrefix);
            str.append(":");
            str.append(name);
            str.append(":");
            return str.toString();
        };
    }

    /**
     * 自定义缓存key生成方式
     */
    @Bean
    public KeyGenerator customKeyGenerator(){
        return new KeyGenerator() {
            //根据target类、调用的方法method、传递的参数params组织spring cache的key
            @Override
            public Object generate(Object target, Method method, Object... params) {
                String key = target.getClass().getSimpleName() + "_" + method.getName() + "_" + StringUtils.arrayToDelimitedString(params, "_");
                return key;
            }
        };
    }

    /**
     * 注册自定义的缓存处理类
     * cacheManager 为使用的缓存管理器
     * 使用CacheResolver来接收,它属于父接口
     * 使用方式,指明处理器: @Cacheable(cacheNames = "yyyy",key = "#root.args[0]",cacheResolver = "redisExpireCacheResolver")
     */
    @Bean
    public CacheResolver redisExpireCacheResolver(CacheManager cacheManager){
        return new RedisExpireCacheResolver(cacheManager);
    }
}

CacheExpire.java:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * cache注解类,可以配置cache失效时间
 */
//此注解作用于方法上
@Target(ElementType.METHOD)
//此注解保留到编译成class文件,加载到jvm中也依然存在
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheExpire {

    /**
     * 失效时间,默认60秒
     */
    public long ttl() default 60L;

    /**
     * 失效单位,默认秒
     */
    public TimeUnit unit() default TimeUnit.SECONDS;

    /**
     * 今天有效,失效时间是从当创建这一刻起,到晚上凌晨12点整,是一个动态的时间
     */
    public boolean today() default false;
}

RedisConfig.java:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * redis配置类
 */
@Configuration
public class RedisConfig {


    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //添加时间日期格式的处理
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(dtf));
        javaTimeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(dtf));
        om.registerModule(javaTimeModule);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

RedisExpireCacheResolver.java:

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.cache.interceptor.SimpleCacheResolver;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.*;

/**
 * 使用继承的方式对cache处理器进行扩展
 */
public class RedisExpireCacheResolver extends SimpleCacheResolver {

    public RedisExpireCacheResolver(CacheManager cacheManager) {
        super(cacheManager);
    }

    //重写处理cache的方法
    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        //直接参考父父类AbstractCacheResolver的resolveCaches方法
        //获取当前注解中的缓存名,通过父类进行获取
        Collection<String> cacheNames = getCacheNames(context);
        if (cacheNames == null) {
            return Collections.EMPTY_LIST;
        } else {
            Collection<Cache> result = new ArrayList(cacheNames.size());
            Iterator cacheIterator = cacheNames.iterator();
            while (cacheIterator.hasNext()) {
                String cacheName = (String) cacheIterator.next();
                //通过缓存名获取到缓存对象
                Cache cache = this.getCacheManager().getCache(cacheName);
                if (cache == null) {
                    throw new IllegalArgumentException("Cannot find cache named '" + cacheName + "' for " + context.getOperation());
                }
                //跟AbstractCacheResolver类的resolveCaches的方法比较,只有这里新增解析注解,反射替换RedisCacheConfiguration
                this.parseCacheExpire(cache, context);

                result.add(cache);
            }
            return result;
        }
    }

    /**
     * 通过反射替换cache中的RedisCacheConfiguration类已经设置好的过期时间等的值
     * 解析注解,从注解中获取新设置的过期时间,给RedisCacheConfiguration重新赋值
     */
    private void parseCacheExpire(Cache cache, CacheOperationInvocationContext<?> context) {
        Method method = context.getMethod();
        //判断方法是否包含过期时间注解
        if (method.isAnnotationPresent(CacheExpire.class)) {  //包含CacheExpire注解再处理
            //获取到注解
            CacheExpire cacheExpire = method.getAnnotation(CacheExpire.class);
            Duration duration = null;
            //判断注解是否配置的过期时间为今天
            if(cacheExpire.today()){
                //当配置了过期时间为今天,则计算从这一刻到凌晨12点还有多少时间
                duration = Duration.ofSeconds(getSecondsNextEarlyMorning());
            } else {
                //过期时间为用户自己配置,则根据配置的来创建Duration
                duration = Duration.ofSeconds(cacheExpire.unit().toSeconds(cacheExpire.ttl()));
            }
            //转成RedisCache 这个时候cacheConfig是空的,也就让反射有了可乘之机
            RedisCache redisCache = (RedisCache) cache;
            //获取cache里面的RedisCacheConfiguration
            RedisCacheConfiguration cacheConfiguration = redisCache.getCacheConfiguration();
            //新生成一个configuration
            RedisCacheConfiguration cacheConfig = cacheConfiguration;
            //参数需要对应修改
            cacheConfig = cacheConfig.entryTtl(duration);
            //通过反射获取到类型为RedisCacheConfiguration的字段cacheConfig
            Field field = ReflectionUtils.findField(RedisCache.class, "cacheConfig", RedisCacheConfiguration.class);
            //设置可以访问被private修饰的字段值
            field.setAccessible(true);
            //重新设置替换RedisCacheConfiguration
            ReflectionUtils.setField(field, redisCache, cacheConfig);
        }
    }

    //获取当前时间到第二天凌晨的秒数,用于设置redis失效时间为当天
    private Long getSecondsNextEarlyMorning() {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_YEAR, 1);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return (cal.getTimeInMillis() - System.currentTimeMillis()) / 1000;
    }

}

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

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

相关文章

【微信小程序】收藏功能的实现(条件渲染、交互反馈)

&#x1f3c6;今日学习目标&#xff1a;第十九期——收藏功能的实现(条件渲染、交互反馈) &#x1f603;创作者&#xff1a;颜颜yan_ ✨个人主页&#xff1a;颜颜yan_的个人主页 ⏰预计时间&#xff1a;35分钟 &#x1f389;专栏系列&#xff1a;我的第一个微信小程序 文章目录…

django框架

目录简介MVC与MTV模型MVCMTV创建项目目录生命周期静态文件配置&#xff08;无用&#xff09;启动django[启动](https://www.cnblogs.com/xiaoyuanqujing/articles/11902303.html)路由分组无名分组有名分组路由分发反向解析反向解析结合分组名称空间re_path与path自定义转换器视…

vue3项目怎么写路由 + 浅析vue-router4源码

在SPA项目里&#xff0c;路由router基本是前端侧处理的&#xff0c;那么vue3项目中一般会怎么去写router呢&#xff0c;本文就来讲讲vue-router4的一些常用写法&#xff0c;以及和Composition API的结合使用&#xff0c;同时简单讲讲实现原理&#xff0c;让你轻松理解前端route…

【04】FreeRTOS的任务挂起与恢复

目录 1.任务的挂起与恢复的API函数 1.1任务挂起函数介绍 1.2任务恢复函数介绍&#xff08;任务中恢复&#xff09; 1.3任务恢复函数介绍&#xff08;中断中恢复&#xff09; 2.任务挂起与恢复实验 3.任务挂起和恢复API函数“内部实现”解析 3.1vTaskSuspend() 3.2&#…

Prometheus基础

一、何为Prometheus Prometheus受启发于Google的Brogmon监控系统&#xff08;相似的Kubernetes是从Google的Brog系统演变而来&#xff09;&#xff0c;从2012年开始由前Google工程师在Soundcloud以开源软件的形式进行研发&#xff0c;并且于2015年早期对外发布早期版本。2016年…

【基础】Netty 的基础概念及使用

Netty基本概念理解阻塞与非阻塞同步与异步BIO 与 NIOReactor 模型Netty 基本概念Netty 的执行流程Netty 的模块组件Netty 工作原理Netty 的基本使用Netty ServerNetty Client参考文章基本概念理解 阻塞与非阻塞 阻塞与非阻塞是进程访问数据时的处理方式&#xff0c;根据数据是…

系分 - 案例分析 - 系统维护与设计模式

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录系分 - 案例分析 - 系统维护与设计模式典型例题 1题目描述参考答案典型例题 2题目描述参考答案系分 - 案例分析 - 系统维护与设计模式 典型例题 1 题目描述 某企业两年前自主研发的消防集中控制软件系统…

05-requests添加Cookies与正则表达式

第5讲 requests添加Cookies与正则表达式 整体课程知识点查看 &#xff1a;https://blog.csdn.net/j1451284189/article/details/128713764 本讲总结 request代理使用 request SSL request添加Cookies 数据解析方法简介 数据解析&#xff1a;正则表达式讲解 一、requests 代理 …

【23种设计模式】学习汇总(未完结+思维导图)

获取思维导图翻至底部底部&#xff0c;基本概览博客内容&#xff08;暂未完全完善&#xff0c;期待你的持续关注&#xff09; 写作不易&#xff0c;如果您觉得写的不错&#xff0c;欢迎给博主来一波点赞、收藏~让博主更有动力吧&#xff01; 一.相关内容 在软件工程中&#xf…

关系型数据库RDBMS | 字节青训营笔记

一、经典案例 1、红包雨案例 每年春节&#xff0c;抖音都会有红包雨获得 2、事务 事务(Transaction): 是由一组SQL语句组成的一个程序执行单元(Unit)&#xff0c;它需要满足ACID特性 BEGIN; UPDATE account table SET balance balance - 小目标 WHERE name “抖音; UPDATE…

指数加权平均、动量梯度下降法

目录1.指数加权平均(exponentially weighted averages)这里有一年的温度数据。如果想计算温度的趋势&#xff0c;也就是局部平均值(local average)&#xff0c;或者说移动平均值(moving average)&#xff0c;怎么做&#xff1f;&#xff1a;当天的温度&#xff0c;&#xff1a;…

交换机的基本原理(特别是动态ARP、静态ARP、代理ARP)

第六章&#xff1a;交换机的基本配置 二层交换设备工作在OSI模型的第二层&#xff0c;即数据链路层&#xff0c;它对数据包的转发是建立在MAC&#xff08;Media Access Control &#xff09;地址基础之上的。二层交换设备不同的接口发送和接收数据独立&#xff0c;各接口属于不…

esxi宿主机进入维护模式虚拟机不会自动释放【不会自动迁移出去】解决方法、查看辨别宿主机本地空间和存储池、esxi进入存储内部清理空间

文章目录说明虚拟机不自动释放处理过程报错说明宿主机进入维护模式说明手动迁移报错说明直接启动虚拟机报错说明解决方法报错原因分析解决方法查看辨别宿主机本地空间esxi进入存储内部清理空间进入存储池内存储内部空间清理及原则存储空间说明说明 我当前的esxi主机版本为5.5 …

7亿人养活的眼镜行业,容不下一家县城小店

文|螳螂观察 作者| 青月 如果要盘点那些被暴利眷顾的行业&#xff0c;眼镜零售肯定榜上有名。 从上市企业的财报数据来看&#xff0c;国内眼镜零售行业的首家上市公司——博士眼镜&#xff0c;2021年前三季度的平均毛利率超过60%&#xff1b;国内镜片第一股明月眼镜在2021年…

【C进阶】文件操作

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C语言进阶 ⭐代码仓库&#xff1a;C Advanced 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们…

小程序应用生命周期

小程序应用生命周期生命周期介绍应用生命周期钩子函数参数对象页面生命周期页面生命周期-页面参数组件生命周期生命周期介绍 定义 一个组件或者页面生老病死的过程一堆会在特定时期触发的函数 分类 应用生命周期页面生命周期组件生命周期 应用生命周期钩子函数 属性说明onL…

Xpath Helper 在新版Edge中的安装及解决快捷键冲突问题

&#x1f935;‍♂️ 个人主页老虎也淘气 个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f44d;&#x1f3fb; 收藏…

vue2源码分析-keep-alive组件

简介 keep-alive是Vue.js的一个内置组件。它能够将指定的组件实例保存在内存中&#xff0c;而不是直接将其销毁&#xff0c;它是一个抽象组件&#xff0c;不会被渲染到真实DOM中&#xff0c;也不会出现在父组件链中。 具体用法咱们这里就不再细说了&#xff0c;今天主要是探讨…

JavaEE day2 初识web与HTML

初步了解相关知识 关于端口&#xff08;port&#xff09;&#xff1a;一个端口同一时间只能被一个进程监听&#xff0c;但是一个进程可以监听多个端口 URL的标准格式&#xff1a;协议名称&#xff1a;//主机/资源路径&#xff1f;查询字符串#文档片段 一般协议最常见的为htt…

Java基础之《netty(25)—handler链调用机制》

一、netty的handler的调用机制 1、使用自定义的编码器和解码器来说明netty的handler调用机制。 客户端发送long -> 服务器 服务端发送long -> 客户端 2、案例 二、客户端发送给服务端 1、服务端 NettyServer.java package netty.inboundhandlerAndOutboundhandler;i…