缓存与分布式锁

本文详细介绍了缓存的使用,包括将Redis整合为缓存、缓存失效问题及其解决方案,如设置空数据缓存、避免缓存雪崩和缓存击穿。接着讨论了分布式锁的概念和实现,如Redisson的使用,以及Spring Cache的配置和应用。总结了Spring Cache在读写模式下的缓存一致性问题及解决方案。
摘要由CSDN通过智能技术生成

缓存与分布式锁

缓存

缓存使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。
哪些数据适合放入缓存?

  • 即时性、数据致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)
  • 举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。

image-20200704123219514

整合rediss作为缓存

使用过程

  1. 导入依赖,导入spring-boot-starter-data-redis 依赖会帮我注入RedisAutoConfiguration这个配置类,这个配置类里面有RedisTemplate<Object, Object>StringRedisTemplate

    1. 这里不适用lettuce作为redis 的客户端,而是另外导入jedis。

    2. 因为使用lettuce会出现OutOfDirectMemoryError(堆外内存溢出

    3. 1)、springboot2.0 以后默认使用lettuce 作为操作redis的客户端。而lettuce 使用netty进行网络通信。
      2)、但是lettuce一直存在bug导致netty堆外内存溢出。而netty如果没有指定堆外内存默认使用jvm参数-Xmx300m
      我们也可以通过-Dio.netty.maxDirectMemory进行设置,
      解决方案:不能使用 去调大 -Dio.netty.maxDirectMemory 堆外内存,只能修改lettuce源码
      1)、设计lettuce客户端(改源码)。2)、切换使用jedis(缺点很久没更新了)
      
    4. 概念的理解:

      • redisTemplate: 是对redis各个客户端的同一API,方便我们程序员使用
      • lettuce、jedis 是操作redis的底层客户端。Spring再次封装成redisTemplate
  2. 配置redis地址信息

  3. 从IOC 容器中取出Spring 封装好的RedisTemplate

Spring-boot-redis-starter

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!-- 排除lettuce 使用jedis 操作redis -->
    <exclusions>
        <exclusion>
            <artifactId>lettuce-core</artifactId>
            <groupId>io.lettuce</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>
spring:
  redis:
      host: 192.168.1.10
      port: 6379

api的使用

public class Xxx{
   
@Autowired
StringRedisTemplate stringRedisTemplate;
 
@Test
public void test1(){
   
  stringRedisTemplate.opsForValue().get("catalogJSON");
  stringRedisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogs), 1, TimeUnit.DAYS);// 缓存一天
  
  Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "xxxxx", 300, TimeUnit.SECONDS);// set lock 11 EX 300 NX
  
   // 定义脚本 http://www.redis.cn/commands/set.html
  String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
  // 泛型是为了序列化结果,这是官方建议的,分布式上锁,最好使用lua脚本来执行
  Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
  
  // 对象的序列化和反序列化(json)
  String json = JSON.toJSONString(catalogs);
  Map<String, List<Catelog2Vo>> catalogs = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
   });
 }
  
}

缓存失效问题

概述

缓存穿透:查询一个不存在的商品,如果我们设置查询结果为null就不放入缓存。那么有人恶意攻击发送百万并发查询这个不存在的数据,那么由于我们的业务逻辑是查询不存在的商品就不放入缓存,这就导致这百万并发都会去查询数据库。那么我们设置的缓存就没有任何意义了。解决办法:如果查询的数据为null,我们依然存入缓存但是设置一个有效期。(查询永不存在的数据

缓存雪崩:就是我们存入缓存的数据同时到了有效期,数据大面积消失,刚好有人查询了这些大面积失效的数据,由于缓存中没有,所以还是会出现大量的数据库操作。解决办法,设置缓存有效期时加上一个随机的时间,保证缓存有效期不一样。(缓存大面积失效

缓存击穿:某一个数据失效,但是这个数据是热点数据,经常出现百万并发查询。但这个数据失效时如果出现百万并发,那么就会发送百万次数据库操作。解决办法:加锁,同一时间只让一个人去查,后续人拿到锁查询时在缓存就有数据了。(某一热点key消失

image-20200704125251765

image-20200704125306900

image-20200704125326867

解决缓存失效问题

缓存穿透:允许缓存空数据,给空数据设置短的有效期。

缓存雪崩:每个key设置不同的有效期。

缓存击穿:上锁。本地锁、分布式锁。

缓存数据一致性

缓存数据一致性解决方案:双写模式、失效模式。

canal 是阿里开源的产品,可以模拟成mysql的从数据库。当我们对mysql操作是肯定会产生binlog(二进制日志)。canal就假装从数据库读取到二进制日志。

image-20200704125922782

image-20200704125933054

image-20200704125947537

image-20200704130002079

本地锁

业务逻辑:调用获取数据的方法 -> 查询缓存 -> 没命中缓存 -> 上锁(synchronized)-> 再次查询缓存 -> 查询数据库、将结果存入缓存 -> synchronized代码块结束 -> 返回数据。

// 这里只是上锁的那部分代码
public /*synchronized*/ Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithLocalLock() {
   
        // 只要是同一把锁,就能锁住这个锁的所有线程。
        // synchronized (this);SpringBoot 所有的组件在容器中都是单例的,所以同一时刻只有一个线程能访问这个方法,synchronize放在这里是可以实现业务要求的。
        // TODO 本地锁:synchronize,JUS(lock),在分布式情况下,想要锁住所有,必须使用分布式锁(分布式锁缺点效率慢)
        synchronized (this) {
   
            String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
            Map<String, List<Catelog2Vo>> collect2 = null;
            if (StringUtils.isEmpty(catalogJSON)) {
   
                collect2 = getData();
            }

            return collect2;
        }
    }

这里需要注意 查询数据库、将结果存入缓存 需要在锁内执行,保证不会因为网络堵塞的问题,导致还没存入缓存,就被第二个线程进来又查询了数据库。

image-20200704125754597

分布式锁

概述

image-20200704125741344

image-20200704125806833

image-20200704125819423

image-20200704125829489

image-20200704125839544

image-20200704125856112

image-20200704125906116

代码演示

解决方案原子加锁,原子解锁

判断再删除的脚本 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

业务逻辑:调用获取数据的方法 -> 查询缓存 -> 没命中缓存 -> 上锁(synchronized)-> 再次查询缓存 -> 查询数据库、将结果存入缓存 -> synchronized代码块结束 -> 返回数据。

public Map<String, List<Catelog2Vo>> getCatalogJson() {
   
        // 从缓存获取分类数据
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        Map<String, List<Catelog2Vo>> catalogs = null;
        if (StringUtils.isEmpty(catalogJSON)) {
   
            // 缓存没有数据,查询数据库
            System.out.println("缓存不命中....查询数据库....");
            catalogs = getCatalogJsonFromDBWithRedisLock();
            return catalogs;
        }
        // 将json 数据反序列化为对象
        catalogs = JSON.
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值