Redis实战
Redis实战
短信登录
基于Session实现登录
session共享问题
多台Tomcat不共享session存储空间,当请求切换到不同的Tomcat服务时导致数据丢失的问题
session的替代方案应该满足:
- 数据共享
- 内存存储
- key-value结构
基于Redis实现共享session登录
redis代替session考虑的问题
- 选择合适的数据结构
- 设置合适的key
- 选择合适的存储粒度
- 设置合适的有效期
解决状态登录刷新的问题
值得注意,这里需要登录的路径才会走拦截器刷新token有效期,导致不需要登录即未拦截的路径没有刷新token有效期。
商户查询缓存
什么是缓存
缓存就是数据交换的缓存区(称作Cache),是存贮数据的临时的地方,一般读写性能较高。
添加Redis缓存
缓存更新策略
三种常见的缓存读写策略
Cache Aside Pattern(旁路缓存模式)
在更新数据库的同时更新缓存
这是平时比较多的缓存读写模式,比较适合读多写少的场景
- 同步时时,尽量选择删除缓存,在读少写多的场景下,避免对于缓存过多无效的写操作
- 如何保证缓存和数据库数据的同步
- 单体系统:将缓存和数据库操作放到一个事务里
- 分布式事务,利用TTC等分布式事务方案
- 先操作数据库,再删除缓存,原因如下图所示
Read/Write Through Pattern(读写穿透)
将缓存和数据库的同步整合为一个服务,由服务来维护一致性,减轻应用程序的职责。调用者调用该服务,无需关心缓存的一致性的问题。开发比较少遇到的原因是性能问题以及服务开发和维护成本。
Write Behind Caching Pattern(异步缓存写入)
调用者操作缓存后,由其他线程异步的将缓存数据持久化到数据库,保证数据的一致性。缺点是数据一致性难以保证,存在数数据库还没有更新,缓存服务宕机的风险。
常用于一些数据经常变化,但是对数据一致性要求没有太高的场景,比如浏览量、点赞量等。
总结
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,缓存不会生效,这些请求会到存储层去查询,大量请求就会落到数据库中。
攻击者利用不存在的key频繁攻击应用,大量请求攻击数据库,导致数据库压力过大甚至引起数据库服务的宕机。
解决方案
接口校验
在接口层增加权限校验,拦截一些不符合逻辑的请求
缓存空对象
缓存和数据库都没有时,将key-value写成key-空对象写入缓存,设置较短缓存有效期(减少数据一致性带来影响),有效降低攻击者短时间内反复用同一个id暴力攻击
缺点:占用缓存中额外的内存开销,有可能造成短时间内数据不一致的问题
布隆(bloomfilter类似于hash set结构)
用于判断某个元素是否存在于集合中,不存在就直接返回。该过滤器的关键就是hash算法和容器的大小
缺点:实现复杂,存在误判的可能性
缓存空对象
总结
缓存雪崩
缓存雪崩指数据大量缓存的key同时失效或者redis宕机,导致大量请求到达数据库。
解决方案
- 缓存数据的过期时间TTL设置随机,防止大量数据同一时间过期
- 搭建redis集群将热点数据均匀分布到不用的缓存数据库,提高服务的可用性
- 缓存业务添加降低限流策略
- 添加多级缓存(nginx,jvm等)
缓存击穿
缓存击穿问题又称为热点key问题,一个高并发访问并且缓存重建业务较为复杂的Key过期,大量请求访问会在瞬间给数据库带来巨大的压力
解决方案
设置热点数据永不过期
接口限流和熔断降级
加互斥锁
缺点是线程等待,性能较差
- 逻辑过期
基于互斥锁方式解决缓存击穿问题
基于逻辑过期方式解决缓存击穿问题
缓存工具封装
优惠券秒杀
全局唯一ID
全局ID生成器
一种分布式系统下用来生成全局唯一ID的工具
ID的组成部分
- 符号位:1bit,0表示正数
- 时间戳:31bit,以秒为单位
- 序列号:32bit,秒以内的计数器,支持每秒产生2^32不同的ID
实现优惠券秒杀下单
库存超卖问题分析
乐观锁
- 版本号法
通过版本标识数据有没有变化
- CAS(compare and swap)法
使用库存代替版本
总结
一人一单
集群下的并发安全问题
单体模式
synchronized是通过JVM内部的监视器控制线程的
集群模式
分布式锁
分布式锁简介
分布式锁:满足分布式系统或集群模式下多线程可见并且互斥的锁。
分布式锁的实现
基于Redis的分布式锁
获取锁
释放锁
问题
在setnx和expire语句执行之间,服务发生了宕机,锁的过期时间就添加失败
获取锁
分布式锁误删问题
线程1业务阻塞后锁超时过期,线程2拿到了锁执行业务,此时线程1阻塞业务执行完毕,误删了线程2的锁,导致线程3拿到了锁,执行业务,导致了两个线程同时执行同一个业务
解决方法
获取锁标识并判断是否一致,释放锁验证是否是该线程的锁
改进Redis分布式锁
分布式锁的原子性问题
线程1在获取锁标识判断一致后,将要执行释放锁的操作时,线程1发生由于JVM垃圾回收机制等原因发生了阻塞,锁没有被释放但过期后被释放,线程2执行时获取锁,线程1阻塞结束执行了释放锁的操作,导致线程2的锁被释放,线程3此时获取锁导致了两个线程并行执行。
Redis的Lua的脚本
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
Lua是一种编程语言,它的基本语法:https://www.runoob.com/lua/lua-tutorial.html
基本语法
执行脚本
基于Lua脚本的释放锁
1 | -- 比较线程标示与锁中的标示是否一致 |
Java调用lua脚本改造分布式锁
总结
基于Redission的分布式锁优化
基于setnx实现的分布式锁存在下面的问题
- 不可重入:同一线程无法多次获取同一把锁
- 不可重试:获取锁只尝试一次就返回false,没有重试机制
- 超时释放:可以避免死锁,但仍然存在一定的安全隐患。例如业务执行耗时比锁的超时时间长,导致了锁的提前释放等
- 主从一致性:Redis提供了主从集群模式,主从同步存在延迟。当主节点宕机时,从节点并未同步主节点的锁数据时,会导致多个线程拿到锁的情况,产生安全问题
Redission
使用方法
引入依赖
1 | <!-- Redisson --> |
配置客户端
1 |
|
使用分布式锁
1 | // 通过Redisson获取可重入锁 |
Redisson可重入锁原理
获取可重入锁的Lua脚本
释放可重入锁的Lua脚本
总结
利用一个Hash结构记录获取锁的线程和重入的次数,Redisson底层的核心就是Lua脚本
Redis分布式锁原理
- 可重入性:利用ConcurrentMap存储记录线程id和重入次数
- 可重试:利用信号量和PubSub功能实现等待、唤醒、获取锁失败的重试机制
- 超时续约:利用WatchDog,每隔一段时间(leaseTime/3),重置超时时间
Redisson的multiLock原理
Redisson分布式锁的主从一致性问题
总结
秒杀优化
异步秒杀思路
并行
使用Lua脚本判断秒杀库存和一人一单状态,保证执行的原子性
总结
秒杀业务的优化思路
- 利用Redis和Lua脚本完成校验判断和抢单业务
- 将下单业务放到阻塞队列,利用线程池异步下单
基于阻塞队列的异步秒杀的问题
- 阻塞队列的内存限制问题(阻塞队列来自JVM)
- 数据安全问题(阻塞队列中的订单没有被消费完,却发生了重启或宕机等事故)
Redis消息队列
消息队列
消息队列(Message Queue)存放消息的队列,最简单的消息队列模型包括三个角色
- 消费队列:存储和管理消息,称为消息代理(Message Broker)
- 生产者:发送消息到消息队列
- 消费者:从消息队列获取消息并处理消息
Redis提供了三种不同的方式实现消息队列
- List:基于List结构模拟消息队列
- PubSub:基本的点对点消息模型
- Stream:完善的消息队列模型
基于List模拟消息队列
消息队列(Message Queue)存放和管理消息的队列,而Redis的List数据结构是一个双向链表。
基于PubSub的消息队列
PubSub(发布/订阅)是Redis引入的消息传递模型,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有的订阅者都能收到相关信息。
总结
优点
- 采用发布订阅模式,支持多生产、多消费
缺点
- 不支持数据持久化
- 无法避免消息丢失
- 消息堆积存在上限,超出时数据丢失
- 消息订阅者停止时,消息发送会丢失
Stream的单消费模式
Stream是Redis引入的一种新的数据类型,可以实现功能更加完善的消息队列
基于Stream发送消息
基于Stream读取消息
当指定起始ID为$时,代表读取最新的消息,如果处理一条消息的过程中,出现了超过1条以上的消息到达队列,则下次获取时,只能获得最新的一条消息,会出现漏读消息的问题。
总结
优点:
- 消息可回溯
- 一个消息可以被多个消费者读取
- 支持阻塞读取
缺点
有消息漏读的风险
基于Stream的消息队列-消费者组
消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列
- 消息分流:队列中的消息会分流给组内的不同的消费者,而不是重复消费,从而加快消费处理的速度
- 消息提示:消费者组会维护一个标示,记录最后一个被处理的消息,由于stream中的小消息本身就支持持久化,当消费者宕机重启后,消费者还会从标示之后读取消息,确保每一个消息都会被消费(避免消息漏读的情况)
- 消息确认:消费者获取消息后,消息处于pending状态,存入一个pending-list。当处理完成后通过XACK来确认消息,并标记消息为已处理,并从pending-list移除(避免消息丢失的情况)
基于Stream的消费队列实现异步秒杀
达人点评
发布点评笔记
点赞
点赞排行榜
好友关注
关注和取关
共同关注
利用Redis中set数据结构实现共同关注功能。
关注和推送
关注推送也叫Feed流,通过下拉刷新获取新的信息。
拉模式
推模式
推拉结合
推模式实现粉丝推荐
实现关注推送页面的滚动分页查询
按脚标(排名)查询
为了防止score相同的情况下,导致偏移量不准确,所以偏移量应该设置为与上一次分页查询结果中,与最小值一样的元素个数。
附近店铺
GEO就是Geolocation的缩写,代表地理位置,Redis在3.2版本加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度检索数据
常见命令
添加地理数据
计算地理距离
搜索附近地理
附近商铺搜索
用户签到
实现签到功能
统计签到
UV统计
HyperLogLog用法
实现UV统计
配置UV统计拦截器
1 |
|