1 短信登录
基于Session实现:发送验证码;验证码登录注册;校验登录状态(使用拦截器实现,保存在ThreadLocal中);UserDTO类隐藏用户敏感信息
session共享问题:多台tomcat不共享session存储空间,当请求切换到不同服务器会导致数据丢失。
基于Redis实现共享session登录
发送验证码(以手机号为key,保存验证码到Redis);
哈希类型可以将对象中的每个字段独立存储,且内存占用更少;使用随机token为key存储用户数据到Redis
2 商户查询缓存
01缓存
数据交换的缓冲区,临时存储数据的地方,读写性能较高。用来衡量cpu性能
浏览器缓存(静态资源);应用层缓存(tomcat);数据库缓冲(索引);cpu缓存;磁盘缓存
作用:降低后端负载;提高IO效率,降低响应时间。
成本:数据一致性成本;代码维护成本;运维成本。
02添加Redis缓存
在客户端和数据库添加中间层缓存
public Result queryById(Long id) {
String key=CACHE_SHOP_KEY+id;
//1从redis查询缓冲
String shopJson=stringRedisTemplate.opsForValue().get(key);
//2判断存在
if(StrUtil.isNotBlank(shopJson)){
//3存在直接返回
Shop shop=JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
//4不存在查数据库
Shop shop=getById(id);
//5不存在返回错误
if (shop==null){
return Result.fail("店铺不存在");
}
//6存在写入redis返回
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
03缓存更新策略
内存淘汰:内存不足时自动淘汰部分数据,下次查询时更新缓存;一致性差;无维护成本。
超时剔除:给缓存数据添加TTL,到期自动删除缓存;一致性一般;维护成本低。
主动更新:自己编写业务逻辑,修改数据库的同时更新缓存;一致性好;维护成本高。
低一致性需求,使用内存淘汰。例如店铺类型查询
高一致性需求,使用主动更新+超时剔除结合。例如店铺详情查询
主动更新策略
缓存旁路(常用):由缓存的调用者在更新数据库的同时删除缓存;保证缓冲和数据库操作同时成功失败,单体系统将缓冲和数据库放在一个事务,分布式系统利用分布式事务方案;一般先操作数据库再删除缓存
读穿透:缓存和数据库整合成一个服务,由服务来维护一致性,调用者调用服务
写回:调用者只操作缓存,有其他线程异步地将缓存数据持久化到数据库
实现缓存和数据库的双写一致
@Override
@Transactional
public Result update(Shop shop) {
Long id=shop.getId();
if(id==null){
return Result.fail("店铺id不能为空");
}
//1更新数据库
updateById(shop);
//2删除缓冲
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
04缓存问题
**缓存穿透:**客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,所有请求都到达数据库。
解决方案:缓存空对象null(实现简单;额外内存消耗,可能造成短期不一致);布隆过滤(请求先进入布隆过滤器,如果数据不存在则拒绝;内存占用少;实现复杂,存在误判可能)
主动方案:增加id复杂度,避免被猜测id概率;做好数据的基础格式校验;加强用户权限校验
//新增:判断命中值是否为空
if(shopJson!=null){
return Result.fail("店铺信息不存在");
}
//4不存在查数据库
Shop shop=getById(id);
//5不存在返回错误
if (shop==null){
//新增:将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
缓存雪崩:在同一时段有大量的缓存key同时失效,或Redis服务宕机,导致大量请求到达数据库。
解决方案:给不同的key添加TTL随机值;利用Redis集群提高服务的可用性;给缓存业务添加降级限流策略;给缓存业务添加降级限流策略;给业务添加多级缓存
缓存击穿:也叫热点key问题,指被高并发访问且缓存重建业务较复杂的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;
}
//实现缓存重建
//11获取互斥锁
String lockKey="lock:shop:"+id;
Shop shop= null;
try {
boolean isLock=tryLock(lockKey);
//22判断是否获取成功
if(!isLock){
//33失败休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//44成功根据id查询数据库
//4不存在查数据库
shop = getById(id);
Thread.sleep(200);//模拟重建的延时
//5不存在返回错误
if (shop==null){
//新增:将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",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 {
//55释放互斥锁
unlock(lockKey);
}
return shop;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//基于逻辑过期解决缓存击穿
public Shop queryWithLogicalExpire(Long id){
String key=CACHE_SHOP_KEY+id;
//1从redis查询缓冲
String shopJson=stringRedisTemplate.opsForValue().get(key);
//2判断存在
if(StrUtil.isBlank(shopJson)){
//3存在直接返回
return null;
}
//新增:存在判断是否过期
//11先把json反序列化为对象
RedisData redisData=JSONUtil.toBean(shopJson,RedisData.class);
Shop shop=JSONUtil.toBean((JSONObject) redisData.getData(),Shop.class);
LocalDateTime expireTime=redisData.getExpireTime();
//22判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//33未过期直接返回店铺信息
return shop;
}
//44已过期需要换成重建
//441获取互斥锁
String lockKey=LOCK_SHOP_KEY+id;
boolean isLock=tryLock(lockKey);
//442判断是否获取锁成功
if(isLock){
//4421成功,开启线程实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() ->{
try {
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lockKey);
}
});
}
//443返回过期的商铺信息
return shop;
}
05缓存工具封装
封装Redis工具类
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题