商城项目-分布式高级-05-缓存以及分布式加锁

583 阅读9分钟

缓存和分布式锁

缓存的使用

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

哪些数据适合放入缓存中?

  1. 即时性、数据一致性要求不高
  2. 访问量大且更新评率不高的数据(读多,写少)

image-20201201111629261

data = cache.load(id);    // 从缓存中加载数据
if(data == null){
    data = db.load(id);   // 从数据库中加载数据
    cache.put(id,data);   // 放入缓存中
}
return data;

使用redis作为缓存

引入jar包

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

添加配置文件

image-20201201114130223

image-20201201124717725

高并发下的缓存失效

缓存穿透

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

风险

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

解决

null结果缓存,并加入短暂过期时间

缓存雪崩

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

解决

原有的失效时间基础上增加一个随机值

image-20201201140411580

缓存击穿

  • 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被高并发的访问,是一种非常“热点”的数据
  • 如果这个key在大量请求同时进来前刚好失效,那么对这个key的数据查询都落到db上,就是~

解决

加锁,大量并发只让一个去查,其他人就会等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用到db

使用本地锁,保证查询数据和放入redis缓存中是一个原子性操作,这样可以避免查询2次数据库

image-20201201143112999

image-20201201142008339

image-20201201142022275

分布式加锁

在使用本地锁解决上面的问题的情况下,但是在分布式的情况中,如果启动了多个同一个项目就会查询多次,而不是只查一次。

本地锁只能锁住当前进程,而其他进程仍然会进行查询。

基本原理

使用redis 中的==set key value NX==这个命令,在多个同一个服务往redis中设置值的时候,这个命令的作用是如果存在就不设置了。这样就一次只会有一个服务能够执行。

image-20201202120440524

image-20201202125536993

image-20201202125617086

getDataFromDb()就是最开始的方法,提取出来了。
因为getCatalogJsonFromDbWithLocalLock()和getCatalogJsonFromDbWithRedisLock()都重复了这个方法。

分布式锁演进-阶段一(由于业务原因造成死锁)

问题

如果占好锁以后由于业务原因宕机了就会造成==死锁==。

解决

设置锁的自动过期时间,即使没有删除,也会自动删除

image-20201202122828289

image-20201202130443123

分布式锁演进-阶段二(把设置lock和设置过期时间调整为一个原子操作)

image-20201202130639200

分布式锁演进-阶段三

image-20201202132434000

image-20201202133023510

最终-保证获取值进行对比和删除的是一个原子操作

image-20201202135525439

image-20201202135747075

redisson

github.com/redisson

使用

导入jar包

<!--使用redission作为所有分布式锁,分布式对象功能框架-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.14.0</version>
</dependency>

添加配置类

@Configuration
public class RedissonConfig {

    /**
     * 所有对redisson的使用都是通过RedissonClient对象
     *
     * @return {@link RedissonClient}* @throws IOException ioexception
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        // 1、使用单节点模式
        config.useSingleServer()
                .setAddress("redis://47.112.150.204:6379");
        return Redisson.create(config);
    }
}

可重入锁测试

  /**
   * a、锁会自动续期,默认续期到30s,不用担心业务时间过长,导致锁过期被删除
   * b、加锁的业务只要运行完成,就不会续期,当完成后就会在30s内删除
   */
   可重入锁测试

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

Redisson同时还为分布式锁提供了异步执行的相关方法:

RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);

RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.

公平锁

基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

读写锁

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

闭锁(CountDownLatch)

当 gogogo这个方法被调用5次后,lockDoor运行

image-20201207143550381

信号量

总共只有

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

使用redisson分布式加锁

image-20201207145436266

redis缓存数据一致性

1.双写模式 2.失效模式

image-20201207145536763

image-20201207145552788

image-20201207150438274

spring cache

使用

依赖

<!--使用spring cache-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置

CacheAutoConfiguration

  RedisCacheConfiguration

  RedisCacheManager

主要配置都在CacheProperties中

spring:
	cache:
    	type: redis

使用

image-20201209123504471

第一次

image-20201209131115033

image-20201209131135035

第二次,没有调用这个方法

image-20201209131153152

常用注解

@EnableCaching 在配置类上添加配置,开启缓存
  • @Cacheable: Triggers cache population.

    • 触发将会把数据保存到缓存中
  • @CacheEvict: Triggers cache eviction.

    • 触发将会把数据从缓存中删除
  • @CachePut: Updates the cache without interfering with the method execution.

    • 不影响方法执行,更新缓存
  • @Caching: Regroups multiple cache operations to be applied on a method.

    • 组合以上多个操作

      @Caching(cacheable = {
              @Cacheable(value = {"cache1"},key = "#root.method.name"),
              @Cacheable(value = {"cache2"},key = "#root.method.name")
      }, evict = {
              @CacheEvict(value = {"evict1"},key ="#root.method.name" )
              }
      )
      
  • @CacheConfig: Shares some common cache-related settings at class-level.

    • 在类级别共享缓存的相同配置

默认行为

  1. 如果缓存命中,方法不掉用
  2. ==key默认自动生成,缓存指定的value::simpleKey[]自动生成的key值==
  3. ==缓存的value的值,默认实现jdk序列化机制,将序列化后的数据存到redis==
  4. ==过期时间TTL是-1,永不过期==

自定义

  1. 指定缓存使用的key

    image-20201209132602884

    image-20201209132939043

  2. 指定缓存的ttl时间

    可以使用spel表达式

    image-20201209132612885

  3. 指定缓存数据为json格式

抽取配置类

@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)  // 读取配置文件中的配置
public class CacheConfig {

    /**
     * 配置文件中的配置没有用上
     *
     * @return {@link RedisCacheConfiguration}
     */
    @Bean
    public RedisCacheConfiguration  redisCacheConfiguration() {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 序列化机制:使用json格式缓存
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
        // 设置ttl时间
        config = config.entryTtl(Duration.ofDays(1));
        // 默认缓存空值,设置为不缓存空值
//        config = config.disableCachingNullValues();
        return config;
    }
}

不足

读模式

  1. 缓存穿透:

    查询一个null数据,解决:缓存空数据,
    config = config.disableCachingNullValues();
    
  2. 缓存击穿

    大量并发进来同时查一个数据,且这个数据刚好失效:解决加锁 
    默认是没有加锁的
    
  3. 缓存雪崩

    大量的key同时过期,解决:加随机时间(直接指定过期时间即可)
    

写模式

缓存一致性

  1. 读写的加锁,有序进行(读多写少的系统)
  2. 引入canal,感知到mysql的更新操作
  3. 读多写少,直接去数据库中查询就可以

总结

  1. 常规数据可以使用spring cache(读多写少,即时性,一致性要求不高的数据)来可以使用
  2. 特殊数据:特殊设计