谷粒商城高级篇—缓存

缓存

提升系统性能,加速访问

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

商品分类、商品列表、适合缓存并加一个失效时间。

缓存穿透(去看看布隆过滤器)

​ 查询不存在的数据,高并发请求直接到数据库,穿透!

​ 将null结果加入缓存,并设置短暂过期时间

缓存雪崩

​ 相同过期时间,某一时刻同时失效,高并发请求直接到数据库,雪崩!

​ 失效时间加上随机值

缓存击穿

​ 大量请求在缓存失效后的一刻全部落到数据库,击穿!

​ 加锁

加锁问题!

//单体应用,这样加锁没问题~
    
synchronized(this){
    //得到锁后,再去缓存中确定一下,没有再继续查 (双重检测逻辑)
    ...
}

or
    
public synchronized List<CategoryEntity> listWithTreeFromDb() {
    ...
}

本地锁只能锁住当前进程。

本地锁:synchronized、JUC(Lock)

查了两次的错误,是因为释放锁之后再放了缓存,应该放缓存后再释放锁

分布式锁

原理:去同一个地方占坑~,可以去redis,可以去数据库,可以去任何 大家都能访问到的地方,等待可以自旋的方式。

阶段一:占坑。问题:坑位死了 死锁

阶段二:设置锁过期时间。问题:设置时间和占坑原子操作

阶段三:setnxex("lock",111,10s),原子性占坑和删锁。问题:锁过期业务还没执行完 / 删锁删了别人的锁


Redisson 实现分布式锁 (封装了很多东西)

1、添加依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>

2、配置Redisson

@Configuration
public class MyRedissonConfig {
    @Bean(destroyMethod="shutdown")
    RedissonClient redisson() throws IOException {
        // 1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://47.93.215.5:6379").setPassword("bjfu100083");
        // 2、根据配置创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

3、测试可重入锁

@Autowired
RedissonClient redisson;

@ResponseBody
@GetMapping("/hello")
public String hello() {
 	// 1、获取一把锁,只要锁名字一样,就是同一把锁
    RLock lock = redisson.getLock("my-lock");
    // 2、加锁
    lock.lock();//阻塞式等待,以前自己写的自旋
    
    /* 
    	Redisson 锁自动续期,运行期间自动给锁上新30s,不用担心业务时间长锁过期删掉,
    	加锁的业务只要运行完成,就不会自动续期,不手动解锁,默认30s后删除,所以也解决了死锁问题。
    */
    try{
        System.out.println("加锁成功,执行业务。。。"+Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (Exception e) {
        
    } finally {
        // 3、解锁
        System.out.println("释放锁。。。"+Thread.currentThread().getId());
        lock.unlock();
    }
}

如果指定过期时间,不会自动续期

lock.lock(10,TimeUnit.SECONDS); //10秒后解锁
/*
	指定超时时间,默认超时时间就是指定的时间
	未指定超时时间,使用30*1000【LockWatchdogTimeout看门狗的默认时间】
		只要占锁成功,启动定时任务【重新设置过期时间,继续30s】,每隔10s自动续期
		internalLockLeasetime【看门狗时间 / 3 = 10s】
*/

最佳实战

lock.lock(30,TimeUnit.SECONDS);//省掉了整个续期操作,手动解锁

tryLock 指定最多等待时间

boolean res = lock.tryLock(100,10,TimuUnit.SECONDS);
if(res){
    try{
        ...
    } finally {
		lock.unlock();	
    }
}

getFireLock 公平锁,按顺序

RLock fairLock = redisson.getFairLock("anyLock");

ReadWriteLock 读写锁

/*
	保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁。
	写锁没释放,读就必须等待。
	只有读读不互斥,相当于无锁,只会在redis中记录素有的读锁。
	只要有写的存在,都必须等待。
*/
@GetMapping("write")
@ResponseBody
public String writeValue(){
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
    RLock rlock = lock.writeLock();
    String s = "";
    try{
        // 1、改数据 加写锁,读数据加读锁
        rlock.lock();
        s = UUID.randomUUID().toString();
    	Thread.sleep(30000);
        redisTemplate.opsForValue.set("writeValue",s);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rlock.unlock();
    }
    
    return s;
}

@GetMapping("read")
@ResponseBody
public String readValue(){
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
    // 加读锁
    RLock rlock = lock.readLock();
    rlock.lock()
    String s = "";
    try{
        s = redisTemplate.opsForValue.get("writeValue");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rlock.unlock();
    }
    
    return s;
}

Semaphore 信号量,也可用作分布式限流

/*
	车库停车
*/
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
   	RSemaphore park = redisson.getSemaphore("park");
    park.acquire();//读取一个信号,获取一个值,占一个坑
    // park.tryacquire(); //非阻塞等待
    return "ok";
}

@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
   	RSemaphore park = redisson.getSemaphore("park");
    park.release();//释放一个车位
    return "ok";
}


CountDownLatch 闭锁

/*
	放假、锁门
	等待5个班全部走完,锁大门
*/
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() {
    RCountDownLatch door = redisson.getCountdownLatch("door");
    door.trySetCount(5);
    door.await();//等待闭锁都完成
    return "放假啦。。。";
}
@GetMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") Long id) {
    RCountDownLatch door = redisson.getCountdownLatch("door");
    door.countDown();//计数减1
    return id+"班的人都走了。。。";
}

缓存一致性问题

双写模式

失效模式

解决方案

  1. 用户维度数据(订单数据、用户数据),并发几率小,缓存+过期时间
  2. 分类菜单、商品介绍等基础数据,可使用canal订阅binlog的方式
  3. 缓存数据+过期时间足够解决大部分业务对于缓存的要求
  4. 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓,所以适合用读写锁。(允许临时脏数据)

总结:

  • 放入缓存的数据本就不应该是实时性、一致性要求高的。所以缓存+过期时间
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,应该查数据库

Cannal

SpringCache

1、引入依赖

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

2、写配置

#CacheauroConfiguration 会导入 RedisCacheConfiguration
#自动配好了缓存管理器RedisCacheManager
spring.cache.type=redis
spring.cache.redis.time-to-live=3600000
#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true

3、使用缓存

/*
 1、开启缓存功能 @EnableCaching
 2、使用注解完成缓存
	@Cacheable		保存
	@CacheEvict		删除
	@CachePut		不影响方法执行更新缓存
	@Caching		组合多个操作
	@cacheConfig	在类级别共享缓存相同配置
*/

/*
	默认行为:
		如果缓存命中,不调用方法
		key默认自动生成:	缓存的名字::SimpleKey [](自主生成的key值)
		缓存的value值,默认json序列化机制
		默认ttl时间,-1
	自定义
		指定key: 接收一个SpEL
		指定存活时间
		保存为json格式
*/
@Cacheable({"product"})

原理

/*
    CacheauroConfiguration -> RedisCacheConfiguration
    ->自动配好了缓存管理器RedisCacheManager
    ->初始化所有缓存->每个缓存决定用什么配置
    ->如果redisCacheConfiguration有就用自己的,没有就用默认
    ->想改缓存配置。只需给容器中放一个RedisCacheConfiguration即可
    ->就会应用到当前管理器管理的所有缓存分区中
*/

不足

  1. 读模式
    • 缓存穿透 解决:ache-null-value=true √
    • 缓存击穿 解决:加锁?
    • 缓存雪崩 解决:随机时间 √
  2. 写模式
    1. 读写加锁
    2. 引入Cannal
    3. 读多写多,直接去数据库

总结:

  • 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):

    写模式(只要缓存的数据有过期时间就足够了)

  • 特殊数据:特殊设计

posted @ 2022-03-24 00:51  爆辣牛筋丸  阅读(157)  评论(0)    收藏  举报