谷粒商城高级篇—缓存
缓存
提升系统性能,加速访问
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多、写少)
商品分类、商品列表、适合缓存并加一个失效时间。
缓存穿透(去看看布隆过滤器)
查询不存在的数据,高并发请求直接到数据库,穿透!
将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+"班的人都走了。。。";
}
缓存一致性问题
双写模式

失效模式

解决方案
- 用户维度数据(订单数据、用户数据),并发几率小,缓存+过期时间
- 分类菜单、商品介绍等基础数据,可使用canal订阅binlog的方式
- 缓存数据+过期时间足够解决大部分业务对于缓存的要求
- 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓,所以适合用读写锁。(允许临时脏数据)
总结:
- 放入缓存的数据本就不应该是实时性、一致性要求高的。所以缓存+过期时间
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,应该查数据库
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即可
->就会应用到当前管理器管理的所有缓存分区中
*/
不足
- 读模式
- 缓存穿透 解决:ache-null-value=true √
- 缓存击穿 解决:加锁?
- 缓存雪崩 解决:随机时间 √
- 写模式
- 读写加锁
- 引入Cannal
- 读多写多,直接去数据库
总结:
-
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了)
-
特殊数据:特殊设计

浙公网安备 33010602011771号