Redis_实战
Redis_实战
部署:
- 前端:部署在Nginx
- 后端:部署在tomcat
短信登录
session原理:
每一个session都有一个id,当你访问tomcat服务器时,id就自动写到coockie中了,以后请求就带着id,就可以根据id找到session。(每一个浏览器再发请求时都有一个独立的session)
session在服务器端,coockie在客户端。
token:
登录验证时,短信验证码存在session中。用户也保存在session中(登录凭证)。
登录后,用户信息保存在Threadlocal中,便于其他业务需要调用用户信息。
问题:
集群的session共享问题:多台tomcat不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
替换方案需要满足以下条件:
- 数据共享
- 内存存储
- key-value结构
所以,可以用redis替换。
用redis代替session
- 验证码保存在redis,key变成手机号(加一个业务前缀login:code:)同时设置有效期。(保证了唯一key,保证了用户登录时可以根据手机号取到验证码)
- 登录时用户保存在redis,key变成token(随机生成)(返回token给客户端,验证登录),value变成hash类型,记录用户不同字段的信息。
redis代替session需要考虑的问题:
- 选择合适的数据结构。
- 选择合适的key。
- 选择合适的存储粒度。
拦截器改进
新增一个拦截器,将token有效期刷新的功能放在拦截器1,和登录用户相关的功能放在拦截器2,它们之间是串行的。
拦截器1:(拦截一切请求)
- 获取token
- 查询redis的用户
- 保存到ThreadLocal
- 刷新token有效期
- 放行
拦截器2:(拦截需要登陆的请求)
- 查询ThreadLocal的用户
1.1 不存在,则拦截
1.2 存在,则继续
商户查询缓存
什么是缓存:
就是数据交换的缓冲区,是存储数据的临时地方,一般读写性能较高。
缓存的作用:
- 降低后端负载
- 提高读写效率,降低响应时间。
缓存的成本:
- 数据一致性成本。
- 代码维护的成本。
- 运维成本
该项目中使用redis作为缓存,减轻数据库压力。
redis缓存更新策略
- 内存淘汰:不用自己维护,利用redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存。(一致性-差,维护成本-无)
- 超时剔除:给缓存数据添加TTL时间,到期后自动删除缓存,下次查询时更新缓存。(一致性-一般,维护成本-低)
- 主动更新:编写业务逻辑,在修改数据库数据库的同时,更新缓存。(一致性-好,维护成本-高)
业务场景:
- 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存。
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存。
缓存更新策略-主动更新策略
分类:
- Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存。(✅实际使用的最多)
- Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
- Write Behind Caching Pattern:调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。
操作缓存和数据库时有3个问题需要考虑:
- 删除缓存还是更新缓存:
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多(❌)
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(✅)
- 如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务。
- 分布式系统,利用TCC等分布式事务方案。
- (线程安全问题)先操作缓存还是先操作数据库?
- 先删除缓存,再操作数据库。
- 先操作数据库,再删除缓存。
这两种方式都可能出现异常,但第2种出现异常的可能性更低。
缓存更新策略的最佳实践方案
- 低一致性需求:使用redis自带的内存淘汰机制
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作
- 缓存命中则直接返回。
- 缓存未命中则查询数据库,并写入缓存,设定超时。
- 写操作
- 先写数据库,然后再删缓存。写-删
- 要确保数据库与缓存操作的原子性。
缓存穿透的解决策略
缓存穿透:
指的是客户端请求的数据再缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案:
- 缓存空对象null
- 优点:实现简单,维护方便。
- 缺点:
- 额外的内存消耗(设置TTL)
- 可能造成短期的不一致
- 布隆过滤
在客户端和redis之间
- 优点:内存占用较少,没有多余的key
- 缺点:
- 实现复杂
- 存在误判可能
- 其它:做好数据的基础格式校验、加强用户权限校验。(避免访问到数据库中不存在的数据)
缓存雪崩问题及解决方案
缓存雪崩:
是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的key的TTL添加随机值。(避免同时失效)
- 利用redis集群提高服务的可用性。(避免宕机)
- 给缓存业务添加降级限流策略。(快速失败、拒绝服务)
- 给业务添加多级缓存。(类比:多层防弹衣)
缓存击穿问题及解决方案
缓存击穿:
也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:
- 互斥锁(性能较差)(一致性好)
- 优点:
- 没有额外的内存消耗
- 保证一致性
- 实现简单
- 缺点:
- 线程需要等待,性能受影响
- 可能有死锁风险
- 逻辑过期(并不是真的过期)(性能好)
- 优点:线程无需等待
- 缺点:
- 不保证一致性
- 有额外的内存消耗
- 实现复杂
CAP定理
在一个分布式系统中,C一致性consistency,A可用性avalibility,P分区容错性Partition tolerance,这三个要素最多只能同时实现2点,不可能三者兼顾。
优惠券秒杀
全局唯一id
订单表如果用数据库自增id会有以下问题:
- id的规律性太明显。
- 受单表数据量的限制。
全局id生成器
是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
唯一性、递增性、安全性、高可用、高性能。
可以用redis-String,java中返回long-8字节
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息。
ID-64位-8字节
组成:
- 符号位:1b,永远为0
- 时间戳:31b,以秒为单位,可以使用69年-java生成
- 序列号:32b,秒内的计数器,支持每秒产生2^32个不同ID-(redis-String64位生成,有可能超过32位,所以每一天重置一次序列号,保证不超过32位。)(同时也方便统计年、月、日的订单量)
如何拼接: java生成的时间戳
左移32位<<32
位或运算|
redis-String生成的序列号
全局唯一ID生成策略:
- UUID:JDK提供的,16进制,字符串类型,不符合要求
- Redis自增:64位数字
- snowflake雪花算法:64位数字,不依赖redis,对时钟依赖高
- 数据库自增:单独开一张表,用于自增id。(redis自增的数据库版)
Redis自增ID策略:
时间戳+序列号
- 每天一个key,方便统计订单量。
- ID构造是 时间戳 + 计数器
库存超卖问题
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
- 悲观锁:
一般新增数据时使用
认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
- 例如Synchronized、Lock都属于悲观锁。
- 优点:简单粗暴
- 缺点:性能一般
- 乐观锁:
认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
- 如果没有修改则认为是安全的,自己才更新数据。
- 如果已经被其它线程修改,说明发生了安全问题,此时可以重试或异常。
- 优点:性能好
- 缺点:存在成功率低的问题(可以用分段锁思想解决)
乐观锁
一般是更新数据时用
乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有2种:
- 版本号法。查询库存and版本号-再次判断版本号没有被修改才扣减库存and更新版本号。
- CAS法。用库存代替版本号的功能,修改时再次判断库存没有变化(或者库存>0)(如果用默认的CAS会导致业务失败率过高)才扣减库存。
一人一单
通过加锁,对用户id加悲观锁(user_id在常量池中,保证不会生成2个值相同但地址不同的user_id),锁在下单函数外面,下单函数事务提交后才释放锁。(需要获取spring管理的下单函数对象,因为默认使用this对象,它不通过spring管理,不支持事务)(如果不支持事务,会存在问题:事务没提交,就释放锁了)
下单函数中,如果通过用户id+优惠券id查询到了数据,说明用户对该优惠券下过一次单了,返回失败。
集群下的线程并发安全问题
如何集群部署
- 启动2份服务,设置不同端口。
- 修改nginx的config目录下的nginx.conf文件,配置反向代理和负载均衡。(nginx反向代理服务器)(默认通过轮询方式实现负载均衡)
此时有2台JVM,也就有2个常量池,2个锁监视器,所以user_id可以在2个常量池中存在2次,所以会出现线程并发安全问题。
所以,要让多个JVM共用1把锁,这样的锁JDK无法提供,所以需要分布式锁。(跨JVM、跨进程的锁)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)