redis缓存、分布式锁、Spring Cache

redis缓存、分布式锁、Spring Cache

一、缓存简介

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

盘工作。

哪些数据适合放入缓存?

  • 及时性、数据一致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)

二、分布式锁简介

大型网站及应用基本上都是以分布式部署,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

三、缓存失效问题

缓存穿透

​ 缓存穿透是指 查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。

解决方法:缓存空结果、并且设置短的过期时间。

缓存雪崩

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

解决方法:原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

​ 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。

解决方法:加锁。大量并发只让一个人去查,其他人等待,查到之后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去查数据库。

四、缓存数据一致性问题

1、双写模式

数据更新—>写数据库—>写缓存:读到最新数据有延迟,最终一致性

有可能产生脏数据,但在数据稳定、缓存过期以后,又能得到最新的正确数据

2、失效模式

数据更新—>写数据库—>删缓存:读到最新数据有延迟,最终一致性

3、分布式读写锁

分布式读写锁。读数据等待写数据整个操作完成

	@Autowired
    private RedissonClient redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

	/**
     * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
     * 写锁没释放读锁必须等待
     * 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :必须等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁。写也需要等待
     * 只要有读或者写的存都必须等待
     * @return
     */
    @GetMapping(value = "/write")
    @ResponseBody
    public String writeValue() {
        String s = "";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        RLock rLock = readWriteLock.writeLock();
        try {
            //1、改数据加写锁,读数据加读锁
            rLock.lock();
            System.out.println("写锁加锁成功:"+Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            ops.set("writeValue",s);
            //TimeUnit.SECONDS.sleep(30);
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("写锁释放:"+Thread.currentThread().getId());
        }

        return s;
    }

    @GetMapping(value = "/read")
    @ResponseBody
    public String readValue() {
        String s = "";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        //加读锁
        RLock rLock = readWriteLock.readLock();
        try {
            rLock.lock();
            System.out.println("读锁加锁成功:"+Thread.currentThread().getId());
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            s = ops.get("writeValue");
            //try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
            Thread.sleep(30000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("读锁释放:"+Thread.currentThread().getId());
        }

        return s;
    }

五、整合redis

1、引入redis依赖包

lettuce 客户端:

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

注意:这里可能报堆外内存溢出异常(OutOfDirectMemoryError):升级 lettuce 客户端,或使用 jedis 客户端,即可解决堆外内存溢出异常。

jedis 客户端:

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
2、编写application.yml配置文件
spring:
  redis:
    host: 192.168.1.123
    port: 6379
3、给业务中加入缓存

这里使用依赖中自带的 StringRedisTemplate 来操作 Redis。这里存储的值为转化成 JSON 字符串的对象信息。

@Autowired
private StringRedisTemplate redisTemplate;

// 1.先从缓存中读取分类信息
String testJSON = redisTemplate.opsForValue().get("testJSON");
if (StringUtils.isEmpty(testJSON)) {
    // 2. 缓存中没有,查询数据库
    // 3. 查询到的数据存放到缓存中,将对象转成 JSON 存储
    redisTemplate.opsForValue().set("testJSON", JSON.toJSONString("从数据库查出的数据"));
}

六、本地锁

  • 1、先读取redis缓存中数据
  • 2、是否命中
    • 3、命中—>返回结果—>结束
    • 4、未命中—>加本地锁—>再次确认(即查缓存)—>如果有数据直接返回结束—>否则查询数据库—>将数据放入redis缓存中—>返回数据—>结束
//只要是同一把锁,就能锁住需要这个锁的所用线程
//1、synchronized (this):this表示当前对象:spring boot所有的组件在容器中都是单列的
// 本地锁:synchronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁
public String localLockTest() {
   //1、先读取redis缓存中数据
   //2、是否命中
   //3、命中—>返回结果—>结束
   //4、未命中
    synchronized (this) { //加本地锁
    //再次确认(即查缓存)—>如果有数据直接返回结束—>否则查询数据库—>将数据放入redis缓存中—>返回数据—>结束
    }
}

七、分布式锁

本地锁只能锁住当前服务的进程,每一个单独的服务都会有一个进程读取数据库,不能达到只读取依次数据库的效果,所以需要分布式锁。

  • 1、先读取redis缓存中数据
  • 2、是否命中
    • 3、命中—>返回结果—>结束
    • 4、未命中—>加分布式锁—>再次确认(即查缓存)—>如果有数据直接返回结束—>否则查询数据库—>将数据放入redis缓存中—>返回数据—>结束
1、redis分布式锁

redis 中有一个 SETNX 命令,该命令会向 redis 中保存一条数据,如果不存在则保存成功,存在则返回失败。

我们约定保存成功即为加锁成功,之后加锁成功的线程才能执行真正的业务操作。

public String redisLock() {

        //1、占分布式锁(即到redis中占到位置)
        String uuid = UUID.randomUUID().toString();
        // 设置过期时间和加锁必须是同步的,即原子的
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            System.out.println("获取分布式锁成功");
            String dataFromDb;
            try {
                //加锁成功...执行业务(即可以存入数据)
                dataFromDb = getDataFromDb(); //==========需加锁执行的业务=========
            } finally {
                // lua 脚本解锁
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                // 删除锁
                redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                        //Collections.singletonList("lock"), uuid);
                        Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        } else {
            System.out.println("获取分布式锁失败");
            //加锁失败...重试。 synchronized:自旋不断监听是否有释放锁
            //休眠 100ms 重试
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
        }
    }
2、redisson分布式锁

官方文档:https://github.com/redisson/redisson/wiki

  • 锁的自动续期,如果业务时间很长,运行期间自动给锁续期 30 s,不用担心业务时间过长,锁自动过期被删掉;
  • 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动续期,默认也会在 30 s 后解锁;
(1)引入依赖
		<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.6</version>
        </dependency>
(2) 编写redisson配置文件
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Configuration
public class MyRedissonConfig {
    /**
     * 所有对 Redisson 的使用都是通过 RedissonClient
     *
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        // 1、创建配置
        Config config = new Config();
        // Redis url should start with redis:// or rediss://
        config.useSingleServer().setAddress("redis://192.168.1.123:6379");

        // 2、根据 Config 创建出 RedissonClient 实例
        return Redisson.create(config);
    }
}
(3)可重入锁
	@Autowired
    private RedissonClient redisson;	

	@ResponseBody
    @GetMapping(value = "/hello")
    public String hello() {

        //1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock myLock = redisson.getLock("my-lock");

        //2、加锁,可重入锁
        myLock.lock();      //阻塞式等待。默认加的锁都是30s
        //1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题

        // myLock.lock(10,TimeUnit.SECONDS);   //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
        //问题:在锁时间到了以后,不会自动续期
        //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
        //2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
        //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
        // internalLockLeaseTime 【看门狗时间】 / 3, 10s
        try {
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            //3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁
            System.out.println("释放锁..." + Thread.currentThread().getId());
            myLock.unlock();
        }

        return "hello";
    }
(4)读写锁
	@Autowired
    private RedissonClient redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
	/**
     * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
     * 写锁没释放读锁必须等待
     * 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :必须等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁。写也需要等待
     * 只要有读或者写的存都必须等待
     * @return
     */
    @GetMapping(value = "/write")
    @ResponseBody
    public String writeValue() {
        String s = "";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        RLock rLock = readWriteLock.writeLock();
        try {
            //1、改数据加写锁,读数据加读锁
            rLock.lock();
            System.out.println("写锁加锁成功:"+Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            ops.set("writeValue",s);
            //TimeUnit.SECONDS.sleep(30);
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("写锁释放:"+Thread.currentThread().getId());
        }

        return s;
    }

    @GetMapping(value = "/read")
    @ResponseBody
    public String readValue() {
        String s = "";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        //加读锁
        RLock rLock = readWriteLock.readLock();
        try {
            rLock.lock();
            System.out.println("读锁加锁成功:"+Thread.currentThread().getId());
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            s = ops.get("writeValue");
            //try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
            Thread.sleep(30000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("读锁释放:"+Thread.currentThread().getId());
        }

        return s;
    }
(5)信号量(Semaphore)
	/**
     * 信号量(Semaphore)
     * 车库停车
     * 3车位
     * 信号量也可以做分布式限流
     */
    @GetMapping(value = "/park")
    @ResponseBody
    public String park() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.acquire();     //获取一个信号、获取一个值,占一个车位
        boolean flag = park.tryAcquire();
        if (flag) {
            //执行业务
        } else {
            return "error";
        }
        return "ok=>" + flag;
    }

    @GetMapping(value = "/go")
    @ResponseBody
    public String go() {
        RSemaphore park = redisson.getSemaphore("park");
        park.release();     //释放一个车位
        return "ok";
    }
(6)闭锁(CountDownLatch)
	/**
     *  闭锁(CountDownLatch)
     * 放假、锁门
     * 1班没人了
     * 5个班,全部走完,我们才可以锁大门
     * 分布式闭锁
     */
    @GetMapping(value = "/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {

        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);
        door.await();       //等待闭锁完成

        return "放假了...";
    }

    @GetMapping(value = "/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id) {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();       //计数-1
        return id + "班的人都走了...";
    }

八、Spring Cache

1、简介
  • Spring 从 3.1 开始定义了 org.springframework.cache.Cache和org.springframework.cache.CacheManager 接口来统一不同的缓存技术;并支持使用 JCache(JSR-107)注解简化我们开发;
  • Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合;Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
  • 每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。
  • 使用 Spring 缓存抽象时我们需要关注以下两点;
    • 1、确定方法需要被缓存以及他们的缓存策略
    • 2、从缓存中读取之前缓存存储的数据
2、引入依赖
		<!-- 引入redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
		<!--引入SpringCache依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
3、添加application.yml配置
spring:
  redis:
    host: 192.168.1.123
    port: 6379
  cache:
    # 缓存类型
    type: redis
    redis:
      # 缓存过期时间设置一个小时, 毫秒为单位
      time-to-live: 3600000
      #如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
      #key-prefix: CACHE_
      use-key-prefix: true
      #是否缓存空值,防止缓存穿透
      cache-null-values: true
4、编写Java配置文件(可按需求添加)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

   /* @Autowired
    private CacheProperties cacheProperties;*/

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){

        //存入缓存中的数据设置为json格式
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        //让配置文件有效,即设置的失效时间有效
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }
}

5、使用
 /**
     * @Cacheable({"category"}) //代表当前方法的结果需要缓存,如果缓存中有,方法不用调用,
     *                          如果缓存中没有,会调用方法,最后将结果存入缓存中。
     *                          1、key默认生成,这里默认缓存名字category::SimpleKey []
     *                          2、默认ttl时间为 -1,永不失效
     *                          3、缓存中的value值,默认使用jdk序列化机制,将序列化的数据存入redis
     *                          4、如果缓存中有数据,方法不用调用
     *						  5、默认无加锁
     *                     自定义:1、指定缓存中使用的key,key属性指定接受一个SpEL
     *                            2、指定缓存数据的失效时间,可以在配置文件配置
     *                            3、数据保存格式设置为json
  */
//使用sync = true 开启加锁来解决击穿问题
@Cacheable(value = "category", key = "#root.methodName",sync = true)

//使用@Caching组合使用
//失效模式:比如更新数据,先更新数据库,再删除缓存中的数据,再从数据库获取数据时,再存入缓存
@Caching(evict = {
            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
            @CacheEvict(value = "category",key = "'getCatalogJson'")
    })

//指定删除某个分区下的所有数据
@CacheEvict(value = "category",allEntries = true) 
6、常用注解
  • @Cacheable :触发将数据保存到缓存的操作;

  • @CacheEvict : 触发将数据从缓存删除的操作;

  • @CachePut :不影响方法执行更新缓存;

  • @Cacheing:组合以上多个操作;

  • @CacheConfig:在类级别共享缓存的相同配置;

7、spring-cache总结
  • Spring-Cache的不足之处:
    • 1)、读模式
      • 缓存穿透:查询一个null数据。解决方案:缓存空数据
      • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
      • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
    • 2)、写模式:(缓存与数据库一致)
      • 1)、读写加锁。
      • 2)、引入Canal,感知到MySQL的更新去更新Redis
      • 3)、读多写多,直接去数据库查询就行
  • 总结:
    • 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
    • 特殊数据:特殊设计​
  • 原理:
    CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写

九、其他问题参考下面链接(redisson分布式锁、redis缓存淘汰策略、redis八大数据类型)

https://blog.csdn.net/oneby1314/article/details/113789412

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

华婷深深

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值