Spring Boot与缓存

 本文主要介绍  JSR107 ,Spring缓存抽象、整合redis

 1.  JSR107 

 javaee发布了 JSR107缓存规范,其中定义了5个核心接口,分别是 CachingProvider ,CacheManager、Cache、Entry和Expiry

  • CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
  • CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
  • Cache是一个类似于Map的数据结构并临时存储以Key为索引的值、一个Cache仅被一个CacheManager所拥有。
  • Entry 是一个存储在Cache中的key-value对
  • Expiry每一个存储在Cache中的条目有一个定义的有效期,一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

 2. Spring缓存抽象

 由于后来整合要使用jsr107,整个系统难度较大等一系列原因,导致用的比较少,我们更多使用的是spring缓存抽象,其底层却是和jsr107一样。

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并且支持使用JCache(JSR-107)注解简化我们开发。

Cache

缓存接口,定义缓存操作。实现有:RedisCache、EnCacheCache、ConcurrentMapCache等

CacheManager缓存管理器,管理各种缓存(Cache)组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CacheEvict清空缓存
@CachePut缓存更新,保证方法被调用,又希望结果被缓存时使用该注解
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生成策略
serialize缓存数据时value序列化策略
  •  Cache接口为缓存的组件规范定义,包含缓存的各种操作集合。
  • Cache接口下Spring提供了各种xxxCache的实现,如RedisCache、EhCacheCache、ConcurrentMapCache等。
  • 每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
  • 使用spring缓存抽象时,需要关注以下两点
  1.    确定方法需要被缓存以及他们的缓存策略。
  2.    从缓存中读取之前缓存存储的数据。

搭建缓存测试基本环境:

 数据库表创建 

/*
Navicat MySQL Data Transfer

Source Server         : 本地
Source Server Version : 50528
Source Host           : 127.0.0.1:3306
Source Database       : springboot_cache

Target Server Type    : MYSQL
Target Server Version : 50528
File Encoding         : 65001

Date: 2018-10-29 10:54:04
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for department
-- ----------------------------
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `departmentName` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `lastName` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `gender` int(2) DEFAULT NULL,
  `d_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  创建一个新工程,引入以下依赖(idea中):

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

创建javabean对象

  

public class Department {
    private Integer id;
    private String departmentName;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getDepartmentName() {
        return departmentName;
    }

    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }

    public Department() {
        super();
    }

    public Department(Integer id, String departmentName) {
        this();
        this.id = id;
        this.departmentName = departmentName;
    }

    @Override
    public String toString() {
        return "Department[" +
                "id=" + id +
                ", departmentName='" + departmentName + '\'' +
                ']';
    }
}
public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender; //性别 1男 0女
    private Integer dId;

    public Employee() {
    }

    public Employee(Integer id, String lastName, String email, Integer gender, Integer dId) {
        this();
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.dId = dId;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public Integer getdId() {
        return dId;
    }

    public void setdId(Integer dId) {
        this.dId = dId;
    }

    @Override
    public String toString() {
        return "Employee[" +
                "id=" + id +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", gender=" + gender +
                ", dId=" + dId +
                ']';
    }
}

配置数据源:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_cache
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver  # 可以省略不写,可根据连接自动判断
mybatis:
  configuration:  # 开启驼峰命名法
    map-underscore-to-camel-case: true

使用注解版的mybatis

   1) @MapperScan指定需要扫描的mapper接口所在的包

   

2)mapper文件编写

@Mapper
public interface EmployeeMapper {
    @Select("SELECT * FROM  employee WHERE id=#{id}")
   public Employee getEmpById(Integer id);

    @Update("UPDATE employee SET lastName=#{lastName},email=#{email},gender=#{gender},d_id=#{dId} WHERE id=#{id}")
    public void update(Employee employee);

    @Delete("DELETE FROM employee WHERE id=#{id}")
    public void deleteEmpById(Integer id);

    @Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#{lastName},#{email},#{gender},#{dId})")
    public void insertEmp(Employee employee);
}

3)service层代码编写

@Service
public class EmployeeService {
    @Autowired
    EmployeeMapper employeeMapper;

    public Employee getEmp(Integer id) {
        System.out.println("查询 " + id + " 号员工");
        Employee emp = employeeMapper.getEmpById(id);
        return emp;

    }
}

 4)controller层代码编写

@RestController
@RequestMapping("emp")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;

    @GetMapping("/{id}")
    public Employee getEmp(@PathVariable("id") Integer id) {
        return employeeService.getEmp(id);
    }
}

启动项目测试: localhost:8080/emp/1  ,如下所示

测试环境搭建完毕后,就可以体验缓存的使用了。 其步骤如下:

  1. 开启基于注解的缓存  @EnableCaching

  2. 标注缓存注解即可

   @Cacheable  @CacheEvict    @CachePut

我们可以先查看没有加缓存的情况:

   在application.yml中,打开日志输出

logging:
  level:
    com.zhao.springboot.mapper: debug

      浏览器访问 http://localhost:8080/emp/1  ,每刷新一次页面,就会查询一次数据库,如下所示:

这就说明现在是没有缓存的。下面使用缓存:

  在service层的该查询方法上个加入   @Cacheable ,将方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法。

@Cacheable(cacheNames={"emp"}) 
 public Employee getEmp(Integer id) {
        System.out.println("查询 " + id + " 号员工");
        Employee emp = employeeMapper.getEmpById(id);
        return emp;

    }

 重新启动项目,访问  http://localhost:8080/emp/1 ,经过几次刷新,发现控制台只有一次打印,访问http://localhost:8080/emp/2发现有新的内容输出。说明结果已被缓存。

那么,是什么样的一个运行流程呢?

 @Cacheable

 1. 方法运行前,先去查询Cache(缓存组件),按照cacheName指定的名字获取(CacheManager先获取相应的缓存。第一次获取缓存如果没有Cache组件会自动创建)

2. 去Cache中查找缓存的内容,使用一个key,默认就是方法的参数。

  key是按照某种策略生成的, 默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key

   SimpleKeyGenerator生成key的默认策略

    如果没有参数:key=new SimpleKey()

   如果有一个参数:key=参数的值

  如果有多个参数:key=new  SimpleKey(params)

3. 没有查到缓存就调用目标方法

4. 将目标方法返回的结果,放进缓存中

@Cacheable标注的方法执行前先来检查缓存中有没有该数据,默认按照参数的值作为key去查询缓存,如果没有就运行方法并将结果放入缓存,以后再来调用就可以直接使用缓存中的数据

  核心:

   1) 使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件

  2)  key使用keyGenerator生成的,默认是SimpleKeyGenerator,也可以自定义。

@CachePut 即调用方法,又更新缓存数据

运行时机:  

   1). 先调用目标方法

   2). 将目标方法的结果缓存起来

 编写service层的修改员工方法

 @CachePut(value = "emp")
    public Employee update(Employee employee) {
        System.out.println("更新" + employee.getId() + "号员工");
        employeeMapper.update(employee);
        return employee;
    }

如上代码所示,修改员工信息方法,上面加入注解 @CachePut ,为了方便起见,在controller层,使用了@PutMapping注解,进行测试如下:

     1)查询1号员工:查询的结果会放入到缓存中。

     2) 再次查询,查询结果还是之前的结果lastName=张三,gender=1

     3) 更新1号员工 [lastName=zhangsan,gender=0],更新成功后返回的值如下:

 4) 查询1号员工,查询结果如下:

 查询结果却是没有更新前的,其原因如下:更新后,确实将返回的结果也放进缓存了,但是由于没有指定key的名称,所以默认key为传入的employee对象,值:返回的employee对象。于是再次查询,并没有去查数据库,造成数据不一致。

修改如下:  指定key的名称为 id

       @CachePut(value = "emp", key = "#result.id")
//        @CachePut(value = "emp",key = "#employee.id")// 两者都可以
        public Employee update(Employee employee) {
            System.out.println("更新" + employee.getId() + "号员工");
            employeeMapper.update(employee);
            return employee;
        }

  注意:使用result.id 或者employee.id都可以,但是result不能再@Cacheable下使用。执行时机不同。

此时重修更新员工信息后,再次查询数据就一致了,另外,注意,更新后再去查询,也并没有查询数据库,而是查询的缓存。

也就是说 @CachePut即更新了数据库,也更新了缓存。

@CacheEvict :缓存清除

编写service中的delete方法 ,并编写controller中的delete方法 ,如下:

@CacheEvict(value = "emp",key = "#id",allEntries = false,beforeInvocation = false)
        public void deleteEmp(Integer id){
            System.out.println("delete "+id+"号员工");
            employeeMapper.deleteEmpById(id);
        }
    @GetMapping("delete")
    public String delete(Integer id) {
        employeeService.deleteEmp(id);
        return "success";
    }

key:指定要清除的数据

allEntries = false,  allEntries:指定清除这个缓存(value指定的)中所有的数据,默认为false

 当指定为true时,就意味着当删除了1号员工后,emp下的所有员工(1,2号)的缓存都会被清除。
beforeInvocation = false ,缓存的清除是否在方法执行前执行。

   默认代表是缓存清除操作在方法执行后执行,如果出现异常缓存就不会被清除。 当指定为true时,意味着,清除缓存操作是在方法执行前执行,无论方法是否出现异常,缓存都清除。

@Caching  :定义复制的缓存注解

在mapper中新增一个方法,根据lastName查询员工,在service新增方法 ,controller中同样新增,如下:

 @Select("SELECT * FROM  employee WHERE lastName=#{lastName}")
    public Employee getEmpByLastName(String lastName);
 @Caching(
            cacheable = {
                    @Cacheable(value = "emp", key = "#name")
            },
            put = {
                    @CachePut(value = "emp", key = "#result.id"),
                    @CachePut(value = "emp", key = "#result.email")
            }
    )
    public Employee getEmpByLastName(String name) {
        return employeeMapper.getEmpByLastName(name);
    }
@GetMapping("/lastName/{lastName}")
    public Employee getEmpByLastName(@PathVariable("lastName") String lastName) {
        return employeeService.getEmpByLastName(lastName);
    }

此时注意: 当调用 该方法后,会分别以key为id,email,lastName将结果放入缓存中,此时如果在根据id查询, 就直接查询的数据库,而不是查数据库,但是当根据lastName查询时,发现并没有从缓存中,而是每次都查询了数据库,这是由于上面我们指定了 @CachePut ,该注解要求每次查询数据库,并更新缓存。

3. 整合redis

默认使用的是ConcurrentMapCacheManager==ConcurrentMapCache,将数据保存在ConcurrentMap<Object,Object>中,在开发中也可以使用缓存中间件:redis,memcached,ehcache

整合redis ,前提在linux上已经安装了redis ,关于安装redis的安装可以参考这篇文章:redis安装, 如果需要该工具,可以下载这个:

  redis工具

并通过工具进行连接 ,如下所示:  

 即连接成功。

引入redis的starter :在pom文件中添加以下依赖:

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

配置redis

 application.yml配置文件中新增redis配置: spring.redis.host: 192.168.xxx.xxx

可在测试类中简单测试redis 。

 @Autowired
    StringRedisTemplate stringRedisTemplate; //操作字符串的
    @Autowired
    RedisTemplate redisTemplate;   // k-v都是对象的

    @Autowired
    RedisTemplate<Object, Employee> empRedisTemplate;

/**
     * Redis常见5大类型
     * String(字符串),list(列表),Set(集合),Hash(散列),ZSet(有序)
     * stringRedisTemplate.opsForValue();[String字符串]
     * stringRedisTemplate.opsForList(); [list集合]
     * stringRedisTemplate.opsForSet(); [Set集合]
     * stringRedisTemplate.opsForHash(); [Hash散列]
     * stringRedisTemplate.opsForZSet(); [ZSet(有序集合)]
     */
    @Test
    public void testRedis() {
        //给redis 中保存数据
//        stringRedisTemplate.opsForValue().append("msg","hello");
        String val=(String )stringRedisTemplate.opsForValue().get("msg");
        System.out.println(val);
        // 测试 list
//        stringRedisTemplate.opsForList().leftPush("myList","1");
//        stringRedisTemplate.opsForList().leftPush("myList","2");
//        stringRedisTemplate.opsForList().leftPush("myList","3");
    }

    //测试保存对象
    @Test
    public void testObj() {
        Employee employee = employeeMapper.getEmpById(1);
        // 默认如果保存对象,使用jdk序列化机智,序列化后的数据保存到redis中
//        redisTemplate.opsForValue().set("emp-01",employee); //保存后,在查看,发现全部是 \xAc之类的,并不是json形式存入的
        // 1. 将数据以json的方式保存
        //2. redisTemplate默认的序列化规则:改变默认的序列化规则,在MyRedisConfig中配置的
        empRedisTemplate.opsForValue().set("emp-01", employee);
    }

注意:当在redis中放入对象时,该对象需要被序列化(实现Serializable接口 ),如果直接用restTempPlate放入(jdk的序列化器),那么会产生以下的情况 ,并不是以json形式传入的,

 这是因为默认使用的是jdk的序列化机制,而我们在日常生产中,更多使用的是json形式的数据,为此,我们可以自定义一个配置,改变默认的序列化规则(使用json的序列化机制)。配置类如下:

@Configuration
public class MyRedisConfig {
    // 同理,如果要转换别的对象,可以重新定义新的template。
    @Bean
    public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Employee> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 创建一个json的序列化器
        Jackson2JsonRedisSerializer<Employee> jjrs = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        template.setDefaultSerializer(jjrs);
        return template;
    }
}

这样,使用empRedistemplate 再重新存入对象,就变成如下的形式:

 测试缓存:

   原理:CacheManager===Cache 缓存组件来实际给缓存中存取数据

   1) 引入redis的starter ,容器中保存的是RedisCacheManager

   2) RedisCacheManager帮我们自动创建RedisCache来作为缓存组件,RedisCache通过操作redis来缓存数据。

  3)  默认保存数据k-v都是object,利用序列化保存,如何保存为json?

       1. 引入redis的starter ,cacheManager变为RedisCacheManager 

       2. 默认创建的RedisCacheManager操作redis的时候使用的是 jdk的序列化机制(RedisCacheConfiguration.class)

 public static RedisCacheConfiguration defaultCacheConfig() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        registerDefaultConverters(conversionService);
        return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(new StringRedisSerializer()), SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()), conversionService);
    }

  3. 自定义CacheManager。

@Bean
    public RedisCacheManager empCacheManager(RedisConnectionFactory factory) {

        Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));

        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
        return redisCacheManager;
    }

 自定义CacheManager后,怎么使得我们自己的定义的cacheManager生效呢?在RedisCacheConfiguration的注解中可以看到,

 @ConditionalOnMissingBean ,也就是说,如果容器中没有CacheManager,就使用该配置创建缓存管理器,但是容器中有,就会替换原有的,于是我们就可以直接启动项目,测试。 如下:

 另外 ,以上全部是通过注解式的将数据放入缓存,也可以通过编码式的将数据放入缓存,可以在service中引入 自定义的manager,在需要操作缓存的地方,使用manager.getCache(),拿到缓存对象,然后操作数据。

@Service
@CacheConfig(cacheNames = "dep", cacheManager = "deptCacheManager") //因为存在多个cacheManager ,通过该注解,指明使用某个cacheManager
public class DepartmentService {


    @Qualifier("deptCacheManager")  //精确的根据id名为deptCacheManager拿到bean注入
    @Autowired
    RedisCacheManager  deptCacheManager;  //根据名称获取缓存

    /**
     *   编码式 存入缓存
     * @param id
     * @return
     */
    public Department getDepById(Integer id){
        System.out.println("查询部门 " + id);
        Department dept= departmentMapper.getDeptById(id);
         //获取缓存。
        Cache deptCache= deptCacheManager.getCache("dep");
         //放入缓存
        deptCache.put("dept:"+dept.getId(),dept);
        return  dept ;
    }
}

注意:当容器中存在多个cacheManager时, 要有一个主cacheManager,即用 @Primary 标识的manager,另外不同的service操作时,要指定 其使用的cacheManager。

补充: @Cachable() 标识的方法内部缓存失效问题解决。

 如下所示:

  @Transactional(readOnly = true)
    public List<PersonInfo> AllPerson(String tid) {
        List<TPerson> tPerson= mapper.getTPerson(tid);
        if (tPerson== null) {
            return null;
        }
        List<PersonInfo> personInfos= tPerson.stream().map(t-> this.getPersonDetail(t.getId()))
                .collect(Collectors.toList());
        return personInfos;
    }

  该方法使用了@Cachable注解(注解略.....)对返回结果进行缓存,同时方法中同时调用了本类的另一个方法,调用后,却发现getPersonDetail()方法的返回结果并未缓存,这是什么原因呢? 这是由于Spring Cache时基于动态生成的proxy代理来对方法进行切面,如果对象的方法是内部调用(即this引用),而不是外部引用,则会导致proxy失效。 即AllPerson方法又调用getPersonDetail()时并未走代理,Spring并不知道getPersonDetail()有注解。

这里提供一种解决办法:

 定义一个Spring的上下文。

@Component
public class SpringContextUtil implements ApplicationContextAware {
    // Spring应用上下文。
    private static ApplicationContext applicationContext;

    /**
     * 实现 ApplicationContextAware接口的回调方法,设置上下文环境
     *
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * 返回spring上下文。
     *
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 从spring容器中获取对象。
     *
     * @param beanName
     * @return
     * @throws BeansException
     */
    public static Object getBean(String beanName) throws BeansException {
        return applicationContext.getBean(beanName);
    }

    /**
     * 通用类型:  通过字节码获取对象。
     *
     * @param t
     * @param <T>
     * @return
     * @throws BeansException
     */
    public static <T> T getBean(Class<T> t) throws BeansException {
        return applicationContext.getBean(t);
    }
}

然后在上述方法中,通过spring 上下文重新拿到对象,调用方法,即方法将变为如下形式:

  @Transactional(readOnly = true)
    public List<PersonInfo> AllPerson(String tid) {
        List<TPerson> tPerson= mapper.getTPerson(tid);
        if (tPerson== null) {
            return null;
        }
        List<PersonInfo> personInfos= tPerson.stream().map(t-> SpringContextUtil.getBean(PersonService.class) //本类
                .getPersonDetail(t.getId()))
                .collect(Collectors.toList());
        return personInfos;
    }

3. 使用redis在高并发下出现的缓存失效问题

    1) 缓存穿透

        缓存穿透指 查询一个一定不存在的数据,由于缓存中是不命中的,所以会去查询数据库,但数据库也无此记录,同时也没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

        风险:  利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃

        解决方案:  对null结果也进行缓存,并加入短暂的过期时间

    2)缓存雪崩

        缓存雪崩指的是我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB ,DB瞬时压力过重雪崩。

        解决方案:  可以在原有的失效时间内,增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,从而减小集体失效的事件发生几率。

 3) 缓存击穿

    缓存击穿指 对于一些设置了过期时间的key ,如果这些key可能在某些时间点被超高并发的访问,是一种非常热点的数据, 如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询将会同时落到DB,称之为 缓存击穿。

解决方案:  加锁, 大量并发只让一个去查,其他人进行等待,查到以后释放锁,其他人获取锁,先查缓存,就会有数据,而不会再次进入DB查询。


       

    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值