问题:当我们的tomcat服务器是分布式的时候session失效
⭐解决:将验证码以及用户信息保存到Redis中
-
校验用户信息,基于Redis
- key的设置,要保证唯一性,验证码与手机号绑定,而token通过UUID生成
- 数据类型的选择:code->String,token->Hash
- 创建时设定TTL,但是拦截器中每次会刷新token的TTL,保证token不过期
问题:第一个拦截器并非拦截一切路径
⭐解决:新增一个全局拦截器,如果用户处于登录态(即ThreadLocal中保存了用户信息),我们就刷新Redis的TTL
同时因为多个拦截器,所以要设置好优先级order,0最大
策略
- 主动更新策略:更新数据库后,删除redis缓存,因为如果更新数据库就写入缓存,无效的写操作会很多,因为这些写操作如果没有人查询是无效的
- 问题:如何保证数据库和删除缓存的原子性
- 单机系统,利用JMM保证了有序性
- 分布式系统,需要TCC来支持
- 问题:如何保证数据库和删除缓存的原子性
- 内存淘汰:针对那些极少更新的数据
- 超时剔除:作为主动更新的兜底
穿透:redis+mysql
解决方案
- 缓存空对象
- 优点:实现简单
- 缺点:内存消耗/短时间的不一致(通过添加TTL)
- bloom filter
- 优点:内存消耗小(二进制位+多个hash)
- 缺点:实现复杂/存在假阳性的问题
改进
- 增强id的复杂性(路径参数)
- 做一些基础的数据格式校验
场景:大量key同时失效或者redis服务器宕机
解决方案
- 随机TTL
- 集群
- 降级限流策略
- 多级缓存
击穿:热点key过期,并且重建业务较为复杂的key,导致的大量请求
解决方案:
-
锁的选择
这里利用redis中的
setnx
方法(java中的setIfAbsent
方法),只有key不存在时才能创建成功,因为在分布式系统下不能使用synchronized作为锁。而redis本身的操作又是单线程的,保证了锁的唯一性。
将对象前缀+对象id作为key,保证了高并发
-
锁的释放
为了防止死锁出现,使用try-finally结构,保证一定要释放锁,同时给锁设置过期时间
-
不同于,互斥锁的方法,逻辑过期保证了更好的可用性(没有死锁,没有线程等待),但是key永不过期,如果在高并发场景下,在查询数据库线程回写redis期间会导致数据的不一致
-
线程池:
根据阿里巴巴开发手册,必须通过线程池获取线程,而非自己创建线程
private static final ThreadPoolExecutor CACHE_REBUILD_EXECUTOR = new ThreadPoolExecutor( 2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3) );
将之前解决缓存击穿,缓存穿透的解决方案,利用泛型编程整合成工具类
- 完善RedisUtils
分布式系统下的全局唯一ID,要保证
- 唯一性
- 高可用:使用集群
- 递增性:
- 高性能:快速生成
- 安全性:不泄露生成规律
以Long,8字节作为ID号,如下定义
还有其他方案
问题:超卖问题
核心代码
// 3.减少库存,CAS方案,判断当前的库存是否是之前查询到库存->update时库存必须大于0
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
原理
- 本质是利用了mysql的锁机制,因为在RR隔离级别下,update操作默认会添加行锁中的排他锁,从而必然只会存在一个线程可以进行写操作
- 同时需要创建订单,我们需要提交事务
原理
- 利用之前保存在ThreadLocal中的userID,但是synchronized锁锁定的是jvm对象,在单机情况下,可以使用常量池中的String对象来实现对象的唯一性。
- 同时要注意
@Transactional
是通过代理实现事务的,应该要调用代理对象中的方法来实现事务
核心代码
Long userId = UserHolder.getUser().getId();
// 注意锁释放和事务提交的先后顺序
synchronized (userId.toString().intern()) {// 先将id转化为字符串,在从字符串常量池中查找,从而保证对象锁的正确
// 获取代理对象
// 1.如果直接调用方法,spring的事务会失效,因为spring是通过代理来实现事务的
// 2.需要先获得当前的代理对象再通过代理对象来调用方法才能实现事务
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
在集群模式下,synchronized对象锁失效
因为在不同的tomcat服务器中维护着不同的jvm,这些jvm中的变量无法通讯。
解决方案
- 依然是利用Redis的String作为锁,nx保证锁的唯一性,ex作为防止死锁的保底策略,执行完毕finally释放锁
set lock thread1 nx ex 5;
del lock;
情况1
业务阻塞导致的锁超时误删
注意,分布式场景下,这些线程不一定在同一个节点上,仅仅靠 ThreadID 无法保证线程的唯一性
- 在存入线程id时,我们需要区分不同的节点,使用
static final
类型的UUID来区分不同的节点 - 在释放锁的时机,检查锁中的value是否和当前线程id + 节点编号一致
情况2
要保证判断锁标识和释放锁的原子性
解决方案
- lua脚本
总结
- 获取锁:setnx 操作来保证互斥,同时设置过期时间避免死锁,提高安全性
- 释放锁:利用lua脚本来保证原子性,同时利用UUID + key来保证锁的唯一性,防止误删
- 可重入
- 重试机制
- 超时释放
- 主从一致性
可重入锁的基本原理:
- 之前利用
setnx
命令获得锁,我们只会判断是否存在记录重入次数 + 线程ID + 节点标识,判断即可实现可重入锁
结论
- 使用hash结构
- Redisson重读源码
Redisson的实现方式
- 可重入锁:记录重入次数和线程id
- 重试机制:优秀的重试机制while自旋,但是并非一直轮询,,sleep + 发布订阅 + 信号量功能,如果没有传入的leaseTime,会启用看门狗watchDog定期刷新ttl,从而一直尝试获取锁
- 超时释放
- 主从一致性:mutlilock,取消主从之分,保证每个主从都必须获得锁,执行完写入操作,释放锁。
思路
将 “减库存” “创建订单” 这样的mysql操作和主业务分离,而且将校验下单资格的任务全部放入redis中操作,从而提高性能
设计数据字段
- 库存:String
- 优惠卷对于的购买用户:Set集合
version-1
使用java中的阻塞队列来实现