- session
- 缓存
- 缓存更新方式
- 删除缓存vs更新缓存
- 缓存和数据库操作原子性
- 缓存和数据库操作顺序
- 结论
- 缓存问题
- 缓存穿透
- 缓存雪崩
- 缓存击穿
- 全局唯一ID
- 数据并发线程安全
- 单体
- 分布式
- redis分布式锁的问题
- redis消息队列
- list
- pubsub
- stream
- 消息推送
session
问题:session存在tomcat服务器,多个服务器之间session不共享,nginx会将请求打到不同的服务器
tomcat处理:session拷贝
缺点:
- 存储多份,内存浪费
- 拷贝有延迟
redis处理优点:
- 数据共享
- 内存存储——速度快
- key-value存储 便于查找
key要求唯一、方便携带查找
应当设置ttl,避免存储过长时间,占据内存
value可用json转为string存储,也可适用hash,便于单字段的修改
value中可将一些敏感数据去除
备注:
- cookie中包含sessionId,前端携带cookie到服务器,服务器取出sessionId,查找到session。tomcat自动维护session。
- 每个浏览器不同的session
- 可在redis中存储token-user的键值对,每次操作根据token查询,如果查到,则说明token有效,刷新其ttl。若长期不操作,token过期,redis删除该token,下次使用时无法查到
缓存
优点:
- 降低后端负载
- 效率高,响应快
问题:
- 数据一致性
- 保持数据一致性,代码维护
- 保持缓存高可用,运维
缓存更新方式
- 编码,更新数据库时更新缓存。使用较多
- 服务,服务中维护缓存和数据库一致性,其他方只调用即可。没有现成的服务,自己编写,麻烦。
- 直接操作缓存,另起任务异步更新到数据库。缓存如果挂掉,数据丢失。
删除缓存vs更新缓存
- 更新缓存:多次更新,但可能数据没有使用,多次更新无用
- 删除缓存:删除缓存,查询时,未命中,到数据库查
缓存和数据库操作原子性
- 单体系统:事务
- 分布式:ttc等分布式事务系统
缓存和数据库操作顺序
右边查询速度比左边更新速度快,所以出现上述情况概率高
左边查询速度比右边更新速度快,所以出现上述情况概率低
如果出现,可增加ttl,到期后删除缓存,使得旧数据出现的时间较短
结论
- 低一致性:redis内存淘汰,到期删除
- 高一致性:主动更新+超时剔除
- 读:未命中,查数据库,存入缓存
- 写:写数据库,删缓存,二者原子性
缓存问题
缓存穿透
查询不存在的数据,redis没有,请求会打到数据库。大量并发,数据库承受大量请求
解决:
- 缓存一个空对象
- 简单
- 浪费内存(可加ttl,节省内存)
- 短期不一致(新增数据时主动更新)
- 布隆过滤器:在redis之前,判断数据是否存在,不存在直接结束
- 数据生成hash,转为二进制位,存在布隆过滤器,请求时判断对应的二进制位0/1。内存占用少,实现复杂。
- 不准确,布隆过滤器判断没有则一定没有,判断有可能没有。有穿透风险
- 增加id复杂度,数据格式校验
- 用户权限管理,对用户限流
缓存雪崩
同时大量key失效或redis宕机,大量请求会打到数据库。
解决:
- 给key的TTL增加随机数,使得过期时间分散
- redis集群高可用
- 降级限流策略:快速失败,拒绝服务
- 多级缓存:浏览器缓存(静态资源)、nginx缓存、reids、jvm、数据库。多层面建立缓存
缓存击穿
热点key(高并发访问)失效,缓存重建复杂
解决:
- 互斥锁:缓存重建加锁。。
- 性能差:高并发,只有一个线程重建缓存,其他大量线程都在等待。(获取不到锁,休眠重试)
- 可能死锁
- 逻辑过期:给redis中数据增加过期时间字段。如果查询数据时发现过期,则获取互斥锁开启新线程取重建缓存,自己则使用旧数据。
- 不保证一致性
- 额外内存消耗
- 实现复杂
setnx:没有值的时候设置,有值的时候不能再设置值。类似于互斥锁
用完后删除即可
为避免死锁,可设置TTL(expire lock 5),到期自动删除
如果redis宕机,锁自动释放
//两条命令合在一起 原子性
SET lock 1 EX 5 NX
全局唯一ID
数据库自增id
- 规律性太明显
- 大量数据,一张表放不下,多个表id重复
id=自增id+其他信息拼接而成
解决:
- UUID,jdk生成的16进制的字符串,不是单调递增
- redis自增:时间戳+自增id,key每天一个,使得自增值不会持续增加过大
- snowflake算法,时钟依赖较高,维护一个机器id
- 数据库id,专门一张表,生成自增id,其他表从该处取id。性能较差,可以一次性生成多个,缓存在内存
数据并发线程安全
单体
悲观锁:lock,读写都加锁
乐观锁:读不加锁,写加锁
- 给表加version,先读后写,写时判断verison,保证操作之间数据未改变
- CAS:先读后写,写时判断当前数据和之前数据是否一致
缺点:失败率高,只有一个成功,其他的都失败
分布式
分布式/集群下多进程可见并互斥的
- zk强调一致性,性能不如redis
- redis:setnx等互斥命令
setnx等互斥命令。给key设置了ttl,如果线程1获取锁后长时间阻塞,导致key过期被删除,之后其他线程正常获取锁,线程1唤醒后执行完,del lock(此时的lock是别的线程的锁)。
解决:
- 先get lock 判断lock是否为自己的锁(set lock时存入线程标识),然后del lock。
- 线程标识:使用uuid+线程id,线程id时jvm内部维护的自增id,集群情况下,线程id会重复
get和del是两步操作,不是原子的,del时线程长时间阻塞,key过期被删除,同上
解决:将两个操作放在一起,变成原子性
redis分布式锁的问题
- 不可重入:线程1获取锁,调用线程2,线程2获取锁执行操作时,无法获取,阻塞,死锁
- 不可重试
- 超时删除key
- 主从一致:在主节点获取锁,然后主节点宕机,从节点没有锁
redis消息队列
并发时,为响应速度提高,将部分数据在redis中缓存,在redis中更新数据,将更新的数据存在jvm阻塞队列中,再异步的将数据更新到数据库
问题:
- jvm内存大小有限
- 宕机,数据丢失
list
redis的list做消息队列,数据持久化,不丢失,只能读一次
//没有返回null
LPUSH, RPOP
//阻塞 直到获取到一个可用的,取出并删除
BLPUSH, BRPOP
pubsub
//发布
PUBLISH [channel] [msg]
//订阅
SUBSCRIBE [channel] [msg]
//订阅匹配的
PSUBSCRIBE [channel] [pattern]
- 多生产多消费
- 不可持久化,消息会丢失
- 消息堆积有上限,超出丢失
stream
一种新的数据类型
漏读消息:如果每次只读一条最新消息(设定Id为$),如果一次加入多个消息,则只读到了第一个,其他消息漏掉
消费者组:
- 消息分流,加快处理速度
- 消息标识,标记消费消息的offset,没有漏读问题
- 消息确认,消费完成后,确认,将消息标记为已处理,消息可回溯
- 可阻塞读取
消息推送
FeedLine-TimeLine
- 推送,生产者将消息发给每一个消费者
- 每个消费者存一份消息,内存↑
- 拉取,消费者拉取生产者生成的消息
- 延迟
- 混合:区分不同的生产者和消费者,根据不同情况选择推/拉