第一章-缓存概述

1什么是缓存

缓存的基本思想其实是以空间换时间。我们知道,IO的读写速度相对内存来说是非常比较慢的,通常一个web应用的瓶颈就出现在磁盘IO的读写上。那么,如果我们在内存中建立一个存储区,将数据缓存起来,当浏览器端由请求到达的时候,直接从内存中获取相应的数据,这样一来可以降低服务器的压力,二来,可以提高请求的响应速度,提升用户体验。

2缓存分类

1数据库缓存、2业务缓存、3页面缓存

1数据库缓存

一级缓存:SqlSession级的缓存,在同一个SqlSession中的相同查询语句,只会查询一次。

二级缓存:Mapper(namespace)级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据得到数据会存在二级缓存区域,第二次执行相同sql查询语句,将不会通过数据库,而是直接从内存中获取,提高查询效率。

一级缓存默认开启。 二级缓存需要实现cache接口,去实现其中方法,并在mapper.xml中添加case标签。由于这个类不属于spring管理,所以需要一个@Service中间类为cache接口注入redisTemplate。这种方式只针对单个节点。

3业务缓存

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如Redis),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 Redis集成。

Spring 提供了一套标注来保住我们快速的实现缓存系统: 注意key的引号问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Cacheable 触发添加缓存的方法
可选属性不为Null才进行添加condition = "#oid!=null"

@CacheEvict 触发删除缓存的方法

@CachePut 在不干涉方法执行的情况下更新缓存

  //删除多个缓存
@Caching(
  evict={
 @CacheEvict(value ="usersInfo",key="'user🆔'+#user.id"),
 @CacheEvict(value ="usersInfo",key="'user🆔all'")
  }
)
//所有的@Cacheable()里面都有一个name=“xxx”的属性,这显然如果方法多了,
//写起来也是挺累的,如果可以一次性声明完 那就省事了,
//所以@CacheConfig这个配置,@CacheConfig 不过不用担心,如果你在你的方法写别的名字,那么依然以方法的名字为准。

service中缓存注解

1加入jar包,注意版本

 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
 <dependencies>
 <!--Spring-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.1.9.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.1.9.RELEASE</version>

    <!--redis-->
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-redis</artifactId>
      <version>1.7.6.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
   </dependency>

<!--spring测试-->
<!--    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.1.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>compile</scope>
    </dependency>

    <!--mybatis-plus-->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus</artifactId>
      <version>3.3.1</version>
    </dependency>
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-generator</artifactId>
      <version>3.3.1.tmp</version>
    </dependency>

</project>

2修改spring-dao.xml配置 在redis中加上缓存管理器

1
2
3
4
5
6
7
8
9
<!--配置redisCacheManager-->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager"
>
    <constructor-arg name="redisOperations" ref="redisTemplate" ></constructor-arg>
    <!-- 存活时间 秒-->
    <property name="defaultExpiration" value="15"></property>
</bean>

<cache:annotation-driven />

3service中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 @Override
 @Cacheable(value = "usersInfo",key="'user🆔'+#oid")
 public User findUserById(Integer oid){
     System.out.println("查询一个用户信息,并加到redis"+oid);
     return userMapper.selectById(oid);
 }

//修改一个用户信息,删除单个缓存 和全部缓存。
 //@CacheEvict(value ="usersInfo",key="'user🆔'+#user.id")
 @Caching(
     evict={
    @CacheEvict(value ="usersInfo",key="'user🆔'+#user.id"),
    @CacheEvict(value ="usersInfo",key="'user🆔all'")
     }
 )
public int updateUser(User user){
     return userMapper.updateById(user);
 }

 @Override
 @Cacheable(value ="usersInfo",key="'user🆔all'")
 public List<User> findAllUser() {
     return userMapper.selectList(new QueryWrapper<>());
 }

service中自定义

由于上面的注解设置的过期时间是固定的,那么再缓存过期时,将可能产生缓存雪崩,造成瞬间服务器压力过大。解决办法是,设置自定义的时间。

 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
 @Override
    //编写自定义时间的查询 修改 查询全部
    public User findUserByIdSurvive(Integer oid){
        //若redis中有 就从redis查
        User user = (User) template.opsForValue().get("user🆔"+oid.toString());
        if(user!=null){
            //redis存在 直接返回
            return user;
        }else {
            user=userMapper.selectById(oid);
            template.opsForValue().set("user🆔"+oid,user,100+new Random().nextInt(30), TimeUnit.SECONDS);
            System.out.println("存活时间:"+template.getExpire("user🆔"+oid,TimeUnit.SECONDS));
            return user;
        }
    }

    //编写自定义修改
    @Override
    public int updateUserSurvive(User user) {
        int i = userMapper.updateById(user);
        if(i>0){
            //大于0就进行删除缓存
            template.delete("user🆔"+user.getId());
            template.delete("user:all");
            System.out.println("删除了两个");
        }
        return i;
    }

4页面缓存

就是把热门的页面,如主页,进行缓存到redis中。

第二章-缓存问题

1雪崩

雪崩

指redis中很多数据在同一时间过期,导致大量请求同一时刻请求到数据库。

解决:设置不同的过期时间,在过期时间上加随机数。

2穿透


穿透

产生这个原因是大量的外部攻击,例如:请求大量id不存在的用户进行登录频繁请求接口,导致请求穿透redis频繁请求数据库。

解决:使用bloom过滤器,把用户Id先读入到bloom过滤器,然后对请求进行拦截,bloom过滤器会进行判断,过滤器判断不存在,那么一定不存在。过滤器判断存在,也有可能不存在,bloom会存在一定的误判率。

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在

3击穿


击穿

是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

解决:

可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。

使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。

针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//压力测试 线程池 
CyclicBarrier cyclicBarrier = new CyclicBarrier(1000);

ExecutorService executorService = Executors.newFixedThreadPool(1000);
for (int i = 0; i <1000 ; i++) {
  executorService.execute(new PressureThread(1,userMapper,redisTemplate,
                                             cyclicBarrier));
}
executorService.shutdown();

while(!executorService.isTerminated()){
}
System.out.println("程序结束");

cyclicBarrier.await();