首页 > 数据库 >Redis_实战

Redis_实战

时间:2023-12-23 18:22:26浏览次数:39  
标签:实战 释放 缓存 redis Redis 获取 线程 超时

Redis_实战

部署:

  1. 前端:部署在Nginx
  2. 后端:部署在tomcat

短信登录

session原理:
每一个session都有一个id,当你访问tomcat服务器时,id就自动写到coockie中了,以后请求就带着id,就可以根据id找到session。(每一个浏览器再发请求时都有一个独立的session)

session在服务器端,coockie在客户端。

token:

登录验证时,短信验证码存在session中。用户也保存在session中(登录凭证)。

登录后,用户信息保存在Threadlocal中,便于其他业务需要调用用户信息。

问题:
集群的session共享问题:多台tomcat不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
替换方案需要满足以下条件:

  1. 数据共享
  2. 内存存储
  3. key-value结构
    所以,可以用redis。
用redis代替session
  1. 验证码保存在redis,key变成手机号(加一个业务前缀login:code:)同时设置有效期。(保证了唯一key,保证了用户登录时可以根据手机号取到验证码)
  2. 登录时用户保存在redis,key变成token(随机生成)(返回token给客户端,验证登录),value变成hash类型,记录用户不同字段的信息。

redis代替session'需要考虑的问题:

  1. 选择合适的数据结构。
  2. 选择合适的key。
  3. 选择合适的存储粒度。
拦截器改进

新增一个拦截器,将token有效期刷新的功能放在拦截器1,和登录用户相关的功能放在拦截器2,它们之间是串行的。
拦截器1:(拦截一切请求)

  1. 获取token
  2. 查询redis的用户
  3. 保存到ThreadLocal
  4. 刷新token有效期
  5. 放行

拦截器2:(拦截需要登陆的请求)

  1. 查询ThreadLocal的用户
    1.1 不存在,则拦截
    1.2 存在,则继续

商户查询缓存

什么是缓存:
就是数据交换的缓冲区,是存储数据的临时地方,一般读写性能较高。

缓存的作用:

  1. 降低后端负载
  2. 提高读写效率,降低响应时间。

缓存的成本:

  1. 数据一致性成本。
  2. 代码维护的成本。
  3. 运维成本

该项目中使用redis作为缓存,减轻数据库压力。

redis缓存更新策略
  1. 内存淘汰:不用自己维护,利用redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存。(一致性-差,维护成本-无)
  2. 超时剔除:给缓存数据添加TTL时间,到期后自动删除缓存,下次查询时更新缓存。(一致性-一般,维护成本-低)
  3. 主动更新:编写业务逻辑,在修改数据库数据库的同时,更新缓存。(一致性-好,维护成本-高)

业务场景:

  1. 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存。
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存。
缓存更新策略-主动更新策略

分类:

  1. Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存。(✅实际使用的最多)
  2. Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
  3. Write Behind Caching Pattern:调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。

操作缓存喝数据库时有3个问题需要考虑:

  1. 删除缓存还是更新缓存:
  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多(❌)
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(✅)
  1. 如何保证缓存与数据库的操作的同时成功或失败?
  • 单体系统,将缓存与数据库操作放在一个事务。
  • 分布式系统,利用TCC等分布式事务方案。
  1. (线程安全问题)先操作缓存还是先操作数据库?
  • 先删除缓存,再操作数据库。
  • 先操作数据库,再删除缓存。
    这两种方式都可能出现异常,但第2种出现异常的可能性更低。

缓存更新策略的最佳实践方案

  1. 低一致性需求:使用redis自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
  • 读操作
    • 缓存命中则直接返回。
    • 缓存未命中则查询数据库,并写入缓存,设定超时。
  • 写操作
    • 先写数据库,然后再删缓存。
    • 要确保数据库与缓存操作的原子性。
缓存穿透的解决策略

缓存穿透:
指的是客户端请求的数据再缓存中和数据库中都不存在,这样缓存永远不会失效,这些请求都会打到数据库。

常见的解决方案:

  1. 缓存空对象null
  • 优点:实现简单,维护方便。
  • 缺点:
    • 额外的内存消耗(设置TTL)
    • 可能造成短期的不一致
  1. 布隆过滤
    在客户端和redis之间
  • 优点:内存占用较少,没有多余的key
  • 缺点:
    • 实现复杂
    • 存在误判可能
  1. 其它:做好数据的基础格式校验、加强用户权限校验。(避免访问到数据库中不存在的数据)
缓存雪崩问题及解决方案

缓存雪崩:
是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  1. 给不同的key的TTL添加随机值。(避免同时失效)
  2. 利用redis集群提高服务的可用性。(避免宕机)
  3. 给缓存业务添加降级限流策略。(快速失败、拒绝服务)
  4. 给业务添加多级缓存。(类比:多层防弹衣)
缓存击穿问题及解决方案

缓存击穿:
也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案:

  1. 互斥锁(性能较差)(一致性好)
  • 优点:
    • 没有额外的内存消耗
    • 保证一致性
    • 实现简单
  • 缺点:
    • 线程需要等待,性能受影响
    • 可能有死锁风险
  1. 逻辑过期(并不是真的过期)(性能好)
  • 优点:线程无需等待
  • 缺点:
    • 不保证一致性
    • 有额外的内存消耗
    • 实现复杂

CAP定理
在一个分布式系统中,C一致性consistency,A可用性avalibility,P分区容错性Partition tolerance,这三个要素最多只能同时实现2点,不可能三者兼顾。

优惠券秒杀

全局唯一id

订单表如果用数据库自增id会有以下问题:

  1. id的规律性太明显。
  2. 受单表数据量的限制。

全局id生成器
是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
唯一性、递增性、安全性、高可用、高性能。
可以用redis-String,java中返回long-8字节

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息。

ID-64位-8字节
组成:

  1. 符号位:1b,永远为0
  2. 时间戳:31b,以秒为单位,可以使用69年-java生成
  3. 序列号:32b,秒内的计数器,支持每秒产生2^32个不同ID-(redis-String64位生成,有可能超过32位,所以每一天重置一次序列号,保证不超过32位。)(同时也方便统计年、月、日的订单量)

如何拼接: java生成的时间戳左移32位<<32位或运算| redis-String生成的序列号

全局唯一ID生成策略:

  1. UUID:JDK提供的,16进制,字符串类型,不符合要求
  2. Redis自增:64位数字
  3. snowflake雪花算法:64位数字,不依赖redis,对时钟依赖高
  4. 数据库自增:单独开一张表,用于自增id。(redis自增的数据库版)

Redis自增ID策略:
时间戳+序列号

  1. 每天一个key,方便统计订单量。
  2. ID构造是 时间戳 + 计数器
库存超卖问题

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

  1. 悲观锁:
    一般新增数据时使用
    认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
  • 例如Synchronized、Lock都属于悲观锁。
  • 优点:简单粗暴
  • 缺点:性能一般
  1. 乐观锁:
    认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
  • 如果没有修改则认为是安全的,自己才更新数据。
  • 如果已经被其它线程修改,说明发生了安全问题,此时可以重试或异常。
  • 优点:性能好
  • 缺点:存在成功率低的问题(可以用分段锁思想解决)

乐观锁
一般是更新数据时用
乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有2种:

  1. 版本号法。查询库存and版本号-再次判断版本号没有被修改才扣减库存and更新版本号。
  2. CAS法。用库存代替版本号的功能,修改时再次判断库存没有变化(或者库存>0)(如果用默认的CAS会导致业务失败率过高)才扣减库存。
一人一单

通过加锁,对用户id加悲观锁(user_id在常量池中,保证不会生成2个值相同但地址不同的user_id),锁在下单函数外面,下单函数事务提交后才释放锁。(需要获取spring管理的下单函数对象,因为默认使用this对象,它不通过spring管理,不支持事务)(如果不支持事务,会存在问题:事务没提交,就释放锁了)

下单函数中,如果通过用户id和优惠券id查询到了数据,说明用户对该优惠券下过一次单了,返回失败。

集群下的线程并发安全问题

如何集群部署

  1. 启动2份服务,设置不同端口。
  2. 修改nginx的config目录下的nginx.conf文件,配置反向代理和负载均衡。(nginx反向代理服务器)(默认通过轮询方式实现负载均衡)

此时有2台JVM,也就有2个常量池,2个锁监视器,所以user_id可以在2个常量池中存在2次,所以会出现线程并发安全问题。

所以,要让多个JVM共用1把锁,这样的锁JDK无法提供,所以需要分布式锁。(跨JVM、跨进程的锁)

分布式锁

是什么:
满足分布式系统或集群模式下多进程可见并且互斥的锁。
还要满足其它特性:高可用、高性能、安全性、(可重入不可重入、阻塞非阻塞、公平非公平)

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有3种:

  1. MySQL
  • 互斥:利用MySQL本身的互斥锁机制
  • 高可用:好
  • 高性能:一般
  • 安全性:断开链接,自动释放锁
  1. Redis
  • 互斥:利用setnx这样的互斥命令(数据不存在时才能set成功)(释放时只用删除这个key即可)
  • 高可用:好
  • 高性能:好
  • 安全性:利用锁超时时间,到期释放
  1. Zookeeper
  • 互斥:利用节点的唯一性(不重复)和有序性(递增)实现互斥(一般利用有序性实现互斥,比如最小的节点)(释放锁:删除节点)
  • 高可用:好
  • 高性能:一般
  • 安全性:临时节点,断开连接自动释放
Redis的分布式锁的实现思路

实现分布式锁时需要实现的两个基本方法:

  1. 获取锁
  • 互斥:确保只能有一个线程获取锁
    'SETNX lock thread1'
    调试点1
    'EXPIRE lock 10' 避免服务宕机引起的死锁
    但可能在调试点1发生宕机,这时也会死锁。
    可以这样:
    'SET lock thread1 EX 10 NX'
  • 非阻塞:成功返回true,失败返回false(避免资源浪费)
  1. 释放锁:
  • 手动释放:'DEL lock'
  • 超时释放:获取锁时添加一个超时时间。
Redis分布式锁误删问题

场景:

  1. 线程1业务太长,超过锁的过期时间,锁释放
  2. 线程2获取锁,进入业务,此时线程1执行完业务,释放了线程2的锁,但此时线程2的业务还没执行完。
  3. 线程3获取锁,进入业务,此时发生了线程2、3并发执行业务存在线程安全问题。
    (极端情况)

原因:
线程1把别人的锁删除了。(Redis分布式锁,不同线程共享一把锁)(删除锁时未判断是否是自己的锁)

解决:
(业务正常结束)释放锁的时候,判断锁的标识(可以用UUID表示)(之前用的是线程的ID,在JVM内部维护的,递增,不同JVM可能发生发生碰撞)是否一致。(宕机情况不用判断,直接释放锁即可)
我们加锁时'SET lock thread1 EX 10 NX'
thread1就是锁的标识。

UUID
用UUID区分不同的服务(不同JVM)(Universally Unique Identifier 通用唯一标识码)
用线程ID区分不同线程

分布式锁的原子问题

场景:

  1. 线程1获取锁,执行业务到判断锁是否是自己的,判断完成,下一步就是释放锁了,此时发生了阻塞(比如JVM垃圾回收),这把锁因为超时被释放了。
  2. 线程2在锁释放后获取锁,执行业务。
  3. 线程1执行最后一步释放锁,线程3可以获取锁,并执行业务。此时线程2、3并发执行,会存在并发执行的线程安全问题。

原因:
判断锁 & 释放锁 它们不是原子的,中间存在间隔,在这个间隔内锁被超时释放了。

解决:
保证 判断锁 & 释放锁 这个整体的原子性。
可以考虑redis事务,但是:redis事务支持原子性,不支持一致性,需要结合乐观锁CAS技术,比较复杂。
所以,可以用Lua脚本

Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言。

Lua脚本提供了Redis的调用命令:
基本语法:'redis.call(‘mingling名称’,‘key’,‘其它参数’,...)'

redis调用Lua脚本:
基本语法:' EVAL "return redis.call('set', KEYS[1], ARGV[1]) " 1 name ROSE '

Java中RedisTemplate调用Lua脚本:
'excute(RedisScript script, List keys,Object args);'即可

总结:基于Redis的分布式锁实现思路

思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标识。(nx保证互斥,ex保证超时删除、兜底方案)
  • 释放锁时先判断线程标识是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性。
  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性。
  • (lua脚本保证原子性,防止误删)
  • 利用redis集群保证高可用和高并发特性。
Redisson

基于setnx实现的分布式锁存在下面的问题:

  1. 不可重入。同一个线程无法多次获取同一把锁。
  2. 不可重试。获取锁只尝试1次就返回false,没有重试机制。
  3. 超时释放。锁超时虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患。
  4. 主从一致性。(读写分离模式)如果redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从未同步主中的锁数据,则会出现锁失效。(从升级为主,但是从没有原主的锁数据)

所以,引入redisson

Redisson
是一个在redis的基础上实现的java驻内存数据网络(In-Memory Data Grid)。它不仅提供了一系列的分布式的java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
分布式锁实现:可重入锁、公平锁、联锁、红锁、读写锁、信号量、可过期信号量、闭锁。

使用:

  1. 引入依赖
  2. 配置bean(配置redisson地址)
  3. 使用redisson的分布式锁:1创建锁对象-> 2获取锁 'lock.tryLock(获取锁的最大等待时间(期间会重试),锁的自动释放时间,时间单位);' -> 3释放锁
redisson的可重入锁原理

通过重入次数,记录重入锁。

  1. 利用redis-map数据结构实现。
    key:lock
    value:hahsmap(field:thread1,value:次数)
  2. 通过Lua脚本操作redis的hash数据,保证原子性。
    2.1 同一个线程重入时,次数+1。
    2.2 释放时,次数-1,如果次数<=0,释放锁(del lock)
    2.3 如果不同线程想要获取锁,返回失败(判断线程id不相等)。
Redisson可重试 & leaseTime=-1时超时不释放原理

可重试:订阅并等待锁释放的信号
超时不释放:watchDog

  1. 获取锁流程
    1.1 判断ttl是否null(null:获取锁成功)(剩余有效时间:获取锁失败)
    1.2 获取成功:判断leaseTime是否
    -1(-1永不过期)(!=-1会过期)
    1.3 leaseTime
    -1:开启watchDog(每隔releaseTime1/3的时间间隔,刷新剩余时间)并放回获取锁成功
    1.4 leaseTime!=-1,直接返回获取锁成功。
    1.5 ttl!=null 判断剩余等待时间是否>0(大于0:订阅并等待释放锁的信号->判断等待时间是否超时->超时返回获取锁失败、不超时循环尝试获取锁进入1.1)
  2. 释放锁流程
    2.1 判断是否成功(成功->发送释放锁消息 to 订阅释放锁信号的线程 & 取消watchDog->结束)(失败->记录异常->结束)
总结Redisson分布式锁原理:
  1. 可重入:利用hash结构记录线程id和重入次数。
  2. 可重试:利用信号量和PubSub(发布订阅)功能实现等待、唤醒、获取锁失败的重试机制。(等待、唤醒不会过多占用CPU)
  3. 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间。
主从一致问题-mutiLock联锁

lock = redissonClient.getMultiLock(lock1, lock2, lock3);
在这三个锁中都获取到锁才是获取锁成功。
(有一个不成功就不成功,三个主节点-可以配从节点,如果出现某个主宕机,也不会影响获取锁的结果)(可以配置failedLocksLimit获取锁失败上限)(失败的锁>上限,回退,从头开始获取每一把锁)

分布式锁总结
  1. 不可重入Redis分布式锁
  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁是判断线程标识。
  • 缺陷:不可重入、无法重试、锁超时失效。
  1. 可重入的Redis分布式锁
  • 原理:利用hash结构,记录线程标识和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待。
  • 缺陷:redis宕机引起锁失效问题。
  1. Redisson的multiLock
  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功。
  • 缺点:运维成本高、实现复杂。
  • 优点:所有方案中最安全的一种方案。

加锁导致秒杀业务性能下降,引入秒杀优化

秒杀优化

标签:实战,释放,缓存,redis,Redis,获取,线程,超时
From: https://www.cnblogs.com/espgod/p/17898395.html

相关文章

  • RedissonLock 使用场景以及优缺点分析
    RedissonLock是Redisson库提供的一种基于Redis实现的分布式锁。以下是如何使用RedissonLock以及其优缺点:使用RedissonLock:初始化Redisson客户端:Configconfig=newConfig();config.useSingleServer().setAddress("redis://localhost:6379");RedissonClientredisson......
  • 小白实战教学:开发同城外卖跑腿APP
    本文将以"小白实战教学"为主题,向大家介绍如何从零开始,开发一款简单而实用的同城外卖跑腿APP。 一、准备工作在开始之前,我们需要做一些准备工作。首先,确保你已经安装好了开发环境,包括合适的集成开发环境(IDE)、版本控制工具和相应的编程语言。对于移动应用的开发,通常会选择使用ReactN......
  • redis_原理
    redis_原理数据结构1.动态字符串SDSC语言字符串存在的问题:获取字符串长度需要通过运算非二进制安全不可修改redis构建了一种新的字符串结构,简单动态字符串SimpleDynamicStringSDSRedis是C语言实现的,其中SDS是一个结构体,属性包括:uint8_tlen:buf已保存的字符串字节数......
  • 布隆过滤器:原理介绍与实战
    布隆过滤器用一句话来说,布隆过滤器是为了解决查询一个元素是否存在于某个集合之中。例如:50亿个用户ID,查询某ID是否在这50亿集合之中。50亿*8字节大约为50GB,内存占用极大。所以一般采用位数组,以及位数组的延伸设计:布隆过滤器。在学习布隆过滤器之前,我们需要有些基础性概念:哈......
  • Redis_高级
    Redis_高级分布式缓存单点Redis的问题:数据丢失问题:实现Redis数据持久化并发能力问题:搭建主从集群,实现读写分离故障恢复问题:利用Redis哨兵,实现健康检测和自动恢复存储能力问题:搭建分片集群,利用插槽机制实现动态扩容数据丢失问题-数据持久化RDB基本流程fork主进程获......
  • 实战经验分享:开发同城外卖跑腿小程序
    下文,小编将与大家一同探究同城外卖跑腿小程序的开发实战,包括但不限于技术选型、开发流程、用户体验等多个方面。 1.技术选型在同城外卖跑腿小程序的开发中,技术选型是至关重要的一环。对于前端,选择了使用Vue.js框架,其灵活性和生态系统的支持使得开发过程更加高效。 后端方面,采用了......
  • Spring Boot之@Autowired注解使用区别,实战演示?
    ......
  • 26.基于 page object 模式的测试框架优化实战
    目录异常处理(弹窗黑名单)日志记录报告生成测试数据的数据驱动异常弹框处理定义黑名单列表处理弹框#声明一个黑名单defblack_wrapper(fun):defrun(*args,**kwargs):basepage=args[0]try:returnfun(*args,**kwargs)......
  • LangChain学习三:链-实战
    文章目录上一节内容:LangChain学习二:提示-实战(下半部分)学习目标:明白链是什么?有哪些?怎么用?学习内容一:介绍学习内容二:有那些学习内容三:实战3.1LLMChain3.1.1声明:接入大模型、声明PromptTemplate、LLMChain3.1.2送入大模型3.1.3.多个参数3.2顺序链上一节内容:LangChain学习二:提示-......
  • redis配置允许远程连接
    1、修改redis.conf#允许访问的地址,127.0.0.1为本机,也就是只允许本机访问,修改为0.0.0.0,则可以在任意IP访问bind0.0.0.0#守护进程,修改为yes后即可后台运行daemonizeyes#密码,设置后访问Redis必须输入密码requirepass111222#关闭防护,允许远程连接protected-modeno#监听......