• Spring Cache组件


    《Spring Cache组件》

    提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!



    1. Spring Cache组件概述

    已经清楚了缓存是进行数据操作性能提升的重要手段,所有的数据最终都要分散在磁盘中进行数据的存储,所以在传统的开发中,很多的ORMapping组件(Hibernate、JPA、Mybatis),会进行缓存操作的实现,以提升数据库的访问性能。但是这样的实现方式会存在一个严重的问题,它只能够在数据层上实现,数据层实现缓存虽然很好,但是会存在有一个业务上的偏差,按照正规的设计思想来说,一个业务会牵扯到很多个数据的操作,而且一个业务的展现也可能存在有多个不同的数据层的缓存处理,相当于此时需要在不同的数据层上分别配置缓存,这样的设计就显得非常麻烦,而为了解决业务层上的缓存处理,所以提供了SpringCache缓存支持。

    项目应用中通过缓存组件可以实现应用的处理性能,但是在现实的开发环境中,会存在有不同的缓存组件,例如:常见的单机版缓存组件包括Caffeine、EHCache,常见的分布式缓存组件包括Memcached、Redis。

    在这里插入图片描述

    由于现在的业务层已经提供了缓存的处理支持,所以数据层上就不再需要进行任何的缓存控制了(适合于整合各类的ORM框架),所以为了更好理解SpringCache的处理操作,那么下面首先采用标准的结构进行一个基础的应用设计。为了更好的理解SpringCache的处理操作,那么下面首先采用标准的结构进行一个基础的应用设计。

    2. ConcurrentHashMap缓存管理

    在进行缓存实现的时候,Spring会考虑三种的缓存实现方式:JDK内置的缓存实现(ConcurrentHashMap)、第三方的缓存组件(EHCache、Caffeine)、分布式的缓存实现(Memcached、Redis)。

    ConcurrentHashMap是在J.U.C之中提供的最为重要的技术实现,它可以保证更新安全的前提下,提供良好的数据获取性能,在没有引入任何额外配置的时候,Spring缓存主要使用ConcurrentHashMap操作。

    SpringCache之中为了便于缓存结构的管理,在org.springframework.cache包中提供了两个核心的标准接口,分别是:Cache接口、CacheManager管理接口。

    在这里插入图片描述

    Cache接口规定了缓存数据的保存、增加、失效以及清空处理的操作功能,而想获取到Cache接口实例,那么就需要通过CacheManeger接口方法完成(工厂类型),所有Cache对象都在CacheManager之中保存。

    在这里插入图片描述
    在进行缓存实现过程中,Spring是基于Cache接口提供的方法进行缓存操作的,所以不同的缓存组件如果要接入到Spring之中,则需要提供Cache接口的具体实现子类,考虑到缓存的管理问题,在Spring中又提供了CacheManager接口,所有可以在应用中使用的Cache类型全部在该接口之中进行配置。

    ConcurrentMapCache缓存

    本次将采用SpringCache的默认实现,使用org.springframework.cache.Cache内置的缓存实现类进行处理,当前类是ConcurrentMapCache,通过内置的ConcurrentHashMap属性实现缓存数据的存储。

    public class ConcurrentMapCache extends AbstractValueAdaptingCache {
        private final String name;//SpringCache内置的需要,要提供一个名称
        private final ConcurrentMap<Object, Object> store;//实现缓存数据存储的集合
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 创建一个自定义的CacheConfig配置类,定义CacheManager接口实例
    package com.personal.caffeine.springcache.config;
    
    import org.springframework.cache.Cache;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.cache.concurrent.ConcurrentMapCache;
    import org.springframework.cache.support.SimpleCacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.HashSet;
    import java.util.Set;
    
    @Configuration//配置类
    @EnableCaching//开启缓存
    public class CacheConfig {//缓存配置类
    
        @Bean
        public CacheManager cacheManager() {
            SimpleCacheManager cacheManager = new SimpleCacheManager();//获取缓存管理接口实例
            Set<Cache> caches = new HashSet<>();//保存全部缓存的集合
            caches.add(new ConcurrentMapCache("emp"));//创建一个雇员缓存
            caches.add(new ConcurrentMapCache("dept"));//创建一个部门缓存
            caches.add(new ConcurrentMapCache("sal"));//创建一个工资缓存
            cacheManager.setCaches(caches);//将缓存放入到缓存管理器中
            return cacheManager;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    编写Service层代码

    package com.personal.caffeine.springcache.service;
    
    import com.personal.caffeine.springcache.dao.IEmpDao;
    import com.personal.caffeine.springcache.po.Emp;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    @Service
    public class IEmpService {
    
        @Autowired
        private IEmpDao empDao;
    
        //编辑雇员
        public Emp edit(Emp emp) {
            return empDao.edit(emp);
        }
    
        //删除雇员信息
        public boolean delete(String eid) {
            return empDao.delete(eid);
        }
    
        //根据id查询雇员信息
        @Cacheable(cacheNames = "emp")
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
        //根据名称查询雇员信息
        @Cacheable(cacheNames = "emp")
        public Emp getEname(String ename) {
            return empDao.getEname(ename);
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    编写Dao层代码(模拟查询数据库)

    package com.personal.caffeine.springcache.service;
    
    import com.personal.caffeine.springcache.dao.IEmpDao;
    import com.personal.caffeine.springcache.po.Emp;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    @Service
    public class IEmpService {
    
        @Autowired
        private IEmpDao empDao;
    
        //编辑雇员
        public Emp edit(Emp emp) {
            return empDao.edit(emp);
        }
    
        //删除雇员信息
        public boolean delete(String eid) {
            return empDao.delete(eid);
        }
    
        //根据id查询雇员信息
        @Cacheable(cacheNames = "emp")
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
        //根据名称查询雇员信息
        @Cacheable(cacheNames = "emp")
        public Emp getEname(String ename) {
            return empDao.getEname(ename);
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    编写实体类

    package com.personal.caffeine.springcache.po;
    
    import lombok.Builder;
    import lombok.Data;
    import lombok.ToString;
    
    @Data
    @ToString
    @Builder
    public class Emp {
        private String eid;
        private String ename;
        private String job;
        private Double salary;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    编写测试类

    package com.personal.caffeine;
    
    import com.personal.caffeine.springcache.po.Emp;
    import com.personal.caffeine.springcache.service.IEmpService;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @Slf4j
    @SpringBootTest
    class CaffeineApplicationTests {
    
        @Autowired
        private IEmpService empService;
    
        @Test
        void testGetCache() {
            Emp emp1 = empService.get("1");
            log.info("[第一次查询],emp1:{}", emp1);
    
            Emp emp2 = empService.get("1");
            log.info("[第二次查询],emp2:{}", emp2);
        }
    
        @Test
        void testGetEname() {
            Emp emp1 = empService.getEname("张三");
            log.info("[第一次查询],emp1:{}", emp1);
    
            Emp emp2 = empService.getEname("张三");
            log.info("[第二次查询],emp2:{}", emp2);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    测试结果如下:

    [持久层],ename:张三, emp:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    [第一次查询],emp1:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    [第二次查询],emp2:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    
    • 1
    • 2
    • 3

    一个非常简单的注解,可以直接在业务层上使用,就非常轻松的实现了缓存操作的处理,整体的实现效果是非常简单的,同时也避免影响其他数据层的缓存操作。

    3. @Cacheable详解

    在业务层之中如果想要使用缓存,则在方法上添加@Cacheable注解即可启用,但是实际上@Cacheable注解的内部也是提供有很多属性的。

    Cacheable注解属性

    在这里插入图片描述
    在使用@Cacheable注解的时候,里面会有两个核心的配置属性,一个缓存条件,一个缓存的排除,如果要想进行这两项的配置,那么还需要使用特定的SPEL语法标记。

    缓存的逻辑:缓存空间开始没有任何的数据项,而后通过数据层进行数据加载,随后直接拽入到缓存空间之中,以实现缓存数据的存储,但是现在可能某些数据是不需要进行缓存的,所以必须设置一些缓存配置的条件。

    缓存配置中的SPEL上下文数据

    在这里插入图片描述

    1. 在进行缓存的时候将雇员编号作为缓存的KEY
       //根据id查询雇员信息
        @Cacheable(cacheNames = "emp", key = "#eid")
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    [第一次查询],emp1:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    [第二次查询],emp2:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    
    • 1
    • 2
    • 3
    1. 默认情况下只要进行了数据的查询,那么肯定所有的数据都要求进行缓存处理了,可是现在要求对缓存的数据追加一些标记,判断查询的参数里面包含有指定的字符串才允许缓存。
     //根据id查询雇员信息
        @Cacheable(cacheNames = "emp", key = "#eid", condition = "#eid.contains('1')")
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    eid传入为2时,不符合#eid.contains(‘1’),顾不进行缓存

    [持久层],eid:2, emp:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    [第一次查询],emp1:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    [持久层],eid:2, emp:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    [第二次查询],emp2:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    
    • 1
    • 2
    • 3
    • 4
    1. 将雇员查询结果中工资低于5000的用户不进行缓存处理,需要追加排除配置。
        //根据id查询雇员信息
        @Cacheable(cacheNames = "emp", key = "#eid", unless = "#result.salary<5000")
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当查询结果的salary为10000时,进行了缓存

    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    [第一次查询],emp1:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    [第二次查询],emp2:Emp(eid=1, ename=张三, job=经理, salary=10000.0)
    
    • 1
    • 2
    • 3

    当查询结果的salary为1000时,未触发缓存操作

    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第一次查询],emp1:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第二次查询],emp2:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    
    • 1
    • 2
    • 3
    • 4

    业务层之中的缓存是针对所有数据的,但是如果某些数据在设计的时候没有达到缓存的要求,就将其进行缓存了,这样会造成缓存数据的污染,从而产生严重的缓存处理性能。

    1. 如果说现在是在多线程的处理环境下进行了缓存的查询,有可能会造成缓存穿透问题。现在假设有三个线程查询数据,但是三个线程判断的时候缓存都不存在,所以必然会发生三个线程同时查询数据库操作的可能,此时可以考虑同步缓存操作。
        //根据id查询雇员信息
        @Cacheable(cacheNames = "emp", key = "#eid", unless = "#result.salary<5000", sync = true)
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当启用了同步处理之后,会由一个线程向数据库发出查询指令,而后自动进行缓存处理,而其他等待的线程,等到缓存中有数据之后才会继续进行缓存数据的获取。

    4. Caffeine缓存管理

    使用ConcurrentHashMap实现的缓存处理性能一定不如Caffeine好,因为Caffeine内部在数据实现的结构上会更加的优秀,那么既然现在要使用SpringCache,最佳的做法就是采用Caffeine作为单机的缓存操作。

    如果想要使用不同的缓存组件,其最为核心的话题就是CacheManager以及Cache接口的实现,通过当前的配置项,可以发现,此时的程序内部已经提供了CaffeineCacheManager缓存管理类。

    在这里插入图片描述

    1. 写法一:修改配置类,采用Caffeine缓存:

    添加pom依赖

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-cacheartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    @Configuration//配置类
    @EnableCaching//开启缓存
    public class CacheConfig {//缓存配置类
    
        @Bean
        public CacheManager cacheManager() {
            CaffeineCacheManager cacheManager = new CaffeineCacheManager();
            Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                    .maximumSize(100).expireAfterAccess(3L, TimeUnit.SECONDS);
    
            cacheManager.setCaffeine(caffeine);
            cacheManager.setCacheNames(Arrays.asList("emp"));//设置缓存名称
            return cacheManager;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    缓存生效

    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第一次查询],emp1:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第二次查询],emp2:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    
    • 1
    • 2
    • 3
    1. 写法二:修改配置类,采用Caffeine缓存:
    @Configuration//配置类
    @EnableCaching//开启缓存
    public class CacheConfig {//缓存配置类
    
        @Bean
        public CacheManager cacheManager() {
            SimpleCacheManager cacheManager = new SimpleCacheManager();//获取缓存管理接口实例
            Set<Cache> caches = new HashSet<>();//保存全部缓存的集合
            caches.add(new CaffeineCache("emp", Caffeine.newBuilder().build()));
            cacheManager.setCaches(caches);//将缓存放入到缓存管理器中
            return cacheManager;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    下面给出一个实际使用的案例,可供参考

    package com.chuanglan.geteway.server.config.caffeine;
    
    import java.util.ArrayList;
    import java.util.concurrent.TimeUnit;
    
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.cache.caffeine.CaffeineCache;
    import org.springframework.cache.support.SimpleCacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    
    import com.github.benmanes.caffeine.cache.Caffeine;
    
    @Configuration
    @EnableCaching
    public class CaffeineConfig {
    
    	private static final int DEFAULT_MAXSIZE = 500000;
        private static final int DEFAULT_TTL = 3600 * 24;
    
        /**
         * 定义cache名称、超时时长秒、最大个数
         * 每个cache缺省一个月过期,最大个数500000
         */
        public enum Caches {
        	
        	gateway_v2_settle_account(60 * 5,200),
        	gateway_v2_biz_ext_type(6*60*60,50000),
        	gateway_v2_biz_type(6*60*60,50000),
        	gateway_v2_biz_type_cost_fee(6*60*60,50000),
            gateway_v2_biz_type_activate(6*60*60,50000),
        	gateway_v2_product(6*60*60,5000),
        	gateway_v2_project(6*60*60,500000),
        	gateway_v2_voiceAccounts(5*60,200),
        	gateway_v2_account(6*60*60,500000),
        	gateway_v2_charge_model(6*60*60,500000),
            gateway_v2_cl_living_parent_app_info(24*60*60,500000),
            gateway_v2_cl_living_android_app_info(24*60*60,500000),
            gateway_v2_cl_living_ios_app_info(24*60*60,500000),
            gateway_v2_cl_living_app_info(24*60*60,500000),
            gateway_v2_acc_no_default_project(24*60*60,500000),
            ;
    
            Caches() {
            }
    
            Caches(int ttl) {
                this.ttl = ttl;
            }
    
            Caches(int ttl, int maxSize) {
                this.ttl = ttl;
                this.maxSize = maxSize;
            }
    
            private int maxSize = DEFAULT_MAXSIZE;    //最大數量
            private int ttl = DEFAULT_TTL;        //过期时间(秒)
    
            public int getMaxSize() {
                return maxSize;
            }
    
            public void setMaxSize(int maxSize) {
                this.maxSize = maxSize;
            }
    
            public int getTtl() {
                return ttl;
            }
    
            public void setTtl(int ttl) {
                this.ttl = ttl;
            }
        }
        
    	/**
         * 个性化配置缓存
         */
        @Bean("caffeineCacheManager")
        public CacheManager cacheManager() {
            SimpleCacheManager manager = new SimpleCacheManager();
            ArrayList<CaffeineCache> caches = new ArrayList<CaffeineCache>();
            for (Caches c : Caches.values()) {
                caches.add(new CaffeineCache(c.name(), Caffeine.newBuilder()
                		.recordStats()
                		.expireAfterWrite(c.getTtl(), TimeUnit.SECONDS)
                		.maximumSize(c.getMaxSize()).build()));
            }
            manager.setCaches(caches);
            return manager;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
        @Cacheable(cacheManager = "caffeineCacheManager", cacheNames = CaffeineConstant.CACHE_PRODUCT, key = "'pd' + #projectCode", unless = "#result == null || #result.size() < 1")
        public List<Product> getProd(String projectCode) {
            return productMapper.selectProd(projectCode);
        }
    
    • 1
    • 2
    • 3
    • 4

    5. 缓存更新策略

    并不是所有的数据都一定被保存在缓存之中,会保存在缓存中的数据一般都属于热点数据,所有的热点数据一般都是由客户进行维护的(用户可能是普通的使用者,也有可能是推手),但是也考虑到数据修改的问题,在SpringCache之中是允许使用者进行缓存数据更新的。

    友情提示:非必要不更新。
    在缓存之中保存的数据内容,如果不是特别有需要的时候,千万不要进行更新操作,因为有可能造成缓存热点数据的失效,从而导致数据库之中的查询压力激增,最终导致系统崩溃。

    SpringCache作为一款优秀的缓存组件,它在设计的时候已经考虑到所有数据更新的问题,支持有更新操作,而且这种更新的操作一般也是和业务有直接联系的。

    在这里插入图片描述

    1. 更新缓存代码实现

    注:在缓存里面JakartaEE提供有一个缓存数据操作的标准,这个标准版本号为“JSR-107”,SpringCache支持有该标准。

      //编辑雇员
        @CachePut(cacheNames = "emp", key = "#emp.eid", unless = "#result==null")
        public Emp edit(Emp emp) {
            return empDao.edit(emp);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
     @Test
        void testEditCache() {
            Emp emp1 = empService.get("1");
            log.info("[第一次查询],emp1:{}", emp1);
    
            Emp emp = Emp.builder()
                    .eid("1")
                    .job("经理更新")
                    .ename("张三更新")
                    .salary(1111.0)
                    .build();
    
            empService.edit(emp);
    
            Emp emp2 = empService.get("1");
            log.info("[第二次查询],emp2:{}", emp2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第一次查询],emp1:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [持久层],edit:Emp(eid=1, ename=张三更新, job=经理更新, salary=1111.0)
    [第二次查询],emp2:Emp(eid=1, ename=张三更新, job=经理更新, salary=1111.0)
    
    • 1
    • 2
    • 3
    • 4

    此时的程序代码执行完成之后,可以发现在第一次查询之后,由于缓存之中已经存在有了指定的数据项,所以在进行更新的时候,除了要进行数据表内容的修改之外,也需要进行进行缓存数据的更新处理,所以当第二次发出查询指令的时候,得到的就是缓存之中的新数据项。

    这种缓存的更新操作其实并没有发生另外一次的数据查询(按照基本的做法,缓存数据修改应该先删除,然后进行查询,并且存放新的缓存数据),但是现阶段仅仅是在缓存内容上做了更新的处理,这一点作为缓存来讲已经足够了,但是考虑到性能问题,在高并发情况下一般还是不建议修改缓存数据。

    6. 缓存清除策略

    按照常规的理解,缓存的数据应该与数据库之中的实体数据相对应,所以当数据库之中的数据被删除之后,对应的缓存数据理论上也应该被删除。SpringCache考虑到数据删除的问题,提供有缓存的清除操作。

    实际上很多的系统,可能是缓存还在但是数据已经不存在了,因为缓存的更新相对比数据的更新慢,同时放在缓存中的很多数据一般不会轻易改变。

    在这里插入图片描述

    如果想要实现这种缓存的清除操作,可以使用一个@CacheEvict注解完成,该注解使用的形式和@CachePut注解的形式类似,只需要设置一些删除条件即可。例如:当前是根据雇员的编号进行了数据的缓存配置,那么删除的时候只需要设置上同样的雇员编号即可。

    package com.personal.caffeine.springcache.service;
    
    import com.personal.caffeine.springcache.dao.IEmpDao;
    import com.personal.caffeine.springcache.po.Emp;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheConfig;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    @Service
    @CacheConfig(cacheNames = "emp")//配置公共的缓存信息
    public class IEmpService {
    
        @Autowired
        private IEmpDao empDao;
    
        //编辑雇员
        @CachePut(key = "#emp.eid", unless = "#result==null")
        public Emp edit(Emp emp) {
            return empDao.edit(emp);
        }
    
        //删除雇员信息
        @CacheEvict(key = "#eid")
        public boolean delete(String eid) {
            return empDao.delete(eid);
        }
    
        //根据id查询雇员信息
        @Cacheable()
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
        //根据名称查询雇员信息
        @Cacheable()
        public Emp getEname(String ename) {
            return empDao.getEname(ename);
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    注意:@CacheConfig(cacheNames = “emp”)//配置公共的缓存信息

    	@Test
        void testDeleteCache() {
            Emp emp1 = empService.get("1");
            log.info("[第一次查询],emp1:{}", emp1);
    
            Emp emp2 = empService.get("1");
            log.info("[第二次查询],emp2:{}", emp2);
    
            empService.delete("1");
    
            Emp emp3 = empService.get("1");
            log.info("[第三次查询],emp3:{}", emp3);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第一次查询],emp1:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第二次查询],emp2:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [持久层],delete:1
    [持久层],eid:1, emp:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    [第三次查询],emp3:Emp(eid=1, ename=张三, job=经理, salary=1000.0)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在进行缓存数据的删除之后,除了数据表的数据被清除之外,对于缓存的内容也一并删除了,所以SpringCache在进行缓存的同步处理上确实优秀于其他的操作组件。

    在并发量小的情况下,各种缓存的操作维护可以随意去搞,但是一旦到了高并发的应用场景,这种操作的方式一定不要轻易使用,因为缓存的更新有可能暴露终端数据的操作。

    7. 多级缓存策略

    对于当前的业务层来讲,service中提供了两个数据的查询操作,一个是根据id查询,还有一个是根据名称查询

    //编辑雇员
        @CachePut(key = "#emp.eid", unless = "#result==null")
        public Emp edit(Emp emp) {
            return empDao.edit(emp);
        }
    
        //根据id查询雇员信息
        @Cacheable()
        public Emp get(String eid) {
            return empDao.get(eid);
        }
    
        //根据名称查询雇员信息
        @Cacheable()
        public Emp getEname(String ename) {
            return empDao.getEname(ename);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们首先执行下面的测试代码

     @Test
        void testEditCacheByName() {
            Emp emp1 = empService.getEname("李四");
            log.info("[第一次查询],emp1:{}", emp1);
    
            Emp emp = Emp.builder()
                    .eid("1")
                    .job("总监更新")
                    .ename("李四")
                    .salary(1111.0)
                    .build();
    
            empService.edit(emp);
    
            Emp emp2 = empService.getEname("李四");
            log.info("[第二次查询],emp2:{}", emp2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    执行结果

    [持久层],ename:李四, emp:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    [第一次查询],emp1:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    [持久层],edit:Emp(eid=1, ename=李四, job=总监更新, salary=1111.0)
    [第二次查询],emp2:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    
    • 1
    • 2
    • 3
    • 4

    在当前默认的情况下,edit方法之中的缓存更新操作是以雇员id为主的,但是如果说现在根据姓名查询的时候,这个更新操作可能无法针对于根据姓名的缓存更新。

    通过当前的操作结果可以发现,此时调用了edit方法之后,仅仅实现的是数据表的修改,但是并没有进行缓存数据的更新,所以当再次根据姓名进行数据查询的时候,所查询到的只是缓存之中的旧数据,不更新的原因非常简单,就是因为当前的edit方法并没有配置根据名称更新的缓存处理。

    修改service服务,使其可以实现多级缓存更新配置。

    @Caching(put = {
                @CachePut(key = "#emp.eid", unless = "#result==null"),
                @CachePut(key = "#emp.ename", unless = "#result==null")
        })
        public Emp edit(Emp emp) {
            return empDao.edit(emp);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    执行结果

    [持久层],ename:李四, emp:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    [第一次查询],emp1:Emp(eid=1, ename=李四, job=总监, salary=20000.0)
    [持久层],edit:Emp(eid=1, ename=李四, job=总监更新, salary=1111.0)
    [第二次查询],emp2:Emp(eid=1, ename=李四, job=总监更新, salary=1111.0)
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    【C++】模板初阶
    深度学习与神经网络入门
    社保和五险一金那些事
    idea启动报错javac <options> <source files>
    安卓逆向简介
    部分排序算法讲解
    vue部署宝塔nginx配置(获取用户ip地址、反代理访问api接口、websocket转发)
    SSM+校园好货APP的设计与实现 毕业设计-附源码121619
    vector底层实现及应用注意事项
    正则表达式基础补充学习
  • 原文地址:https://blog.csdn.net/weixin_43695916/article/details/128038078