缓存击穿
概念:缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
两种解决方案:(1)互斥锁;(2)逻辑过期
根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
业务流程:
//获取锁
private boolean tryLock(string key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value: "1", timeout: 10, TimeUnit.SEcONDs);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(string key){
stringRedisTemplate.delete(key);
}
//互斥锁
public shop queryWithMutex(Long id){
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson =stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(strUtil.isNotBlank(shopJson)){
//3.存在,直接返回
return JSONUtil.toBean(shopJson,Shop.class);
}
//判断命中的是否是空值
if(shopJson != null){
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = "lock:shop:" + id;
Shop shop = null;
try{
boolean isLock = tryLock(lockKey);
//4.2.判断是否获取成功
if(!isLock){
//4.3.失败,则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
// 4.4.成功,根据id查询数据库
Shop shop = getById(id),
// 5.不存在,返回错误
if(shop == null){
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key,value:"",CACHE_NULL TTL, TimeUnit.MINUTES);
//返回错误信息
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JsoNutil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
}catch(InterruptedException e){
throw new RuntimeException(e);
}finally{
//7.释放互斥锁
unlock(lockKey);
}
//8.返回
return shop;
}
根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
public void saveShop2Redis(Long id, Long expireseconds){
//1.查询店铺数据
Shop shop = getById(id);
//2.封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3.写入Redis
stringRedisTemplate.opsForValue().set(cACHE SHOP_KEY + id, JsONUtil.toJsonstr(redisData));
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newfixedThreadPool( nthreads: 10);
public shop queryWithLogicExpire(Long id){
String key = CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(strUtil.isNotBlank(shopJson)){
//3.存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil,toBean(shopJson, RedisData.class);
Shop shop = JsoNutil.toBean((JsoNobject)redisData.getData(),shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//5.1.未过期、直接返回店铺信息
return shop;
}
// 5.2 已过期,需要缓存重建
// 6. 缓存重建
// 6.1 获取互斥锁
String lockKey = "lock:shop:" + id;
boolean isLock = tryLock(lockKey);
//6.2.判断是否获取锁成功
if(isLock){
//6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit((){
try {
// 重建缓存
this.saveShop2Redis(id,expireSeconds: 20L);
}catch(Exceptione){
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
});
//6.4 返回过期的商铺信息
return shop;
}
优惠券秒杀
每个店铺都可以发布优惠券:
当用户抢购时,就会生成订单并保存到tb_voucher(优惠券)_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:
- id的规律性太明显
- 受单表数据量的限制
解决方法:全局ID生成器,一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
为了增加ID的安全性,可以不直接使用Redis自增的数值,而是拼接一些其它信息:
ID的组成部分:
- 符号位:1bit,永远为0
- 时间戳:31bit,以秒为单位,可以使用69年
- 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
Redis实现全局唯一ID:
private static final lOng BEGIN_TIMESTAMP = 1640995200L;
private static final int COUNT_BITS = 32;
private stringRedisTemplate stringRedisTemplate;
public RedisIdWorker(stringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate=stringRedisTemplate;
}
public long nextId(string keyPrefix){
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(Zone0ffset.UTC):
long timestamp = nowSecond-BEGIN_TIMESTAMP;
//2.生成序列号
//2.1.获取当前日期,精确到天
String date = now,format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment( key: "icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
全局唯一ID生成策略:
- UUID
- Redis自增
- snowflake算法
- 数据库自增
Redis自增ID策略:
- 每天一个key,方便统计订单量
- ID构造是时间戳+计数器