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

缓存和分布式锁

缓存的使用

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

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

  1. 即时性、数据一致性要求不高
  2. 访问量大且更新评率不高的数据(读多,写少)
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>
复制代码

添加配置文件

高并发下的缓存失效

缓存穿透

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

风险

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

解决

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

缓存雪崩

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

解决

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

缓存击穿

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

解决

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

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

分布式加锁

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

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

基本原理

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

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

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

问题:

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

解决:

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

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

分布式锁演进-阶段三

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

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运行

信号量

总共只有

基于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分布式加锁

redis缓存数据一致性

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

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
复制代码

使用

第一次

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

常用注解

@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

  2. 指定缓存的ttl时间

    可以使用spel表达式

  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. 特殊数据:特殊设计
稀土掘金
我还没有学会写个人说明!
上一篇

新工艺将塑料废料升级改造成更有价值的粘合剂

下一篇

国产美瞳频获融资背后:“小”美瞳的“大”生意经?

你也可能喜欢

评论已经被关闭。

插入图片