当前位置: 首页 > news >正文

redis项目知识体系

短信登录校验功能

拦截器:用户在发送请求的时候需要校验他有没有登录,有登录才能访问资源

不可能在每一个Controller中都写登录校验逻辑,这个时候可以采取拦截器,在请求到每个Controller之前先用拦截器拦截下来,统一进行登录校验,有登录,放行(判断session当中是否有保存该用户的信息),没有登录报错

 因为直接session直接存储user的所有信息,会占用许多内存。

解决办法:创建一个UserDTO对象,将user的部分信息传递到UserDTO中,然后再把UserDTO保存到session当中,ThreadLocal存取也是用UserDTO。

集群的session共享问题

多态Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。用户第一次请求时,被nginx负载均衡到第一台tomcat的时候,保存了他的user信息,但是当用户第二次请求的时候,被nginx负载均衡到第二台tomcat服务器,需要用到用户的user信息,但是第二台服务器并没有存储用户的user信息。

解决办法:用redis来代替session存储用户信息,首先多台tomcat服务器都可以访问redis当中的数据,然后redis的数据是内存存储,速度很快,最后redis存储的数据是key-value型,和session类似。

用session存储user信息的时候,每个浏览器请求都有属于自己的session,通过浏览器请求cookie带来的session的id找到自己的专属session后,就能直接获取某个信息。但是,如果使用的是redis存储信息,比如存储code信息,redis的键值不能重复,每个浏览器请求只能访问到同一个code,我们将redis存储的key值变为phone:手机号 value是验证码,这样的话每个手机号都能存储对应的验证码,在后面的验证手机号和验证码是否匹配的时候,也可以直接在redis查询。

value值如果是验证码自然可以用String类型存储,但是如果value值要存储对象的话,存储类型有两个选择,一个是json,一个是哈希,如果用json的话并不能对单个的字段进行增删改查,所以使用哈希。

将token作为浏览器请求获取相应user数据的标签,在生成token的时候,将token传递给前端,以后就能在浏览器请求的请求头authorization中获得了。token放在内存当中,随着用户的增多,token越来越多,需要的内存也越来越多,这个时候需要设置一个有效期30分钟,我们想要用户如果一直发出请求访问,那么他的token有效期一直就是30分钟,要token过期的条件是用户超过30分钟没有发出请求。如果是用redis的key值过期时间设置有效期,那么只能倒计时30分钟,无法通过判断用户是否访问来重置有效期,这个时候会想到拦截器,拦截器天然可以判断用户请求,如果用我们之前创建好的拦截器的话,有一些请求会拦截,有一些请求不会拦截,那么这个时候就不能很好的根据用户请求重置过期时间,所以额外再添加一个拦截器拦截所有请求。

商户查询缓存功能

redis缓存更新

如果redis中的数据长久不更新,那么可能会出现和mysql数据库数据不一致的问题,通常采用主动更新的方法来保证redis和mysql的数据一致性

在更新数据库后,对缓存的操作是删除还是更新?一般我们会选择删除,因为如果对数据库进行100次写操作更新,那么缓存也要跟着更新100次,如果这个时候没有一个用户访问这个缓存数据,那显然十分不值。但是如果采用删除缓存,就算数据库更新100次,此时没有用户访问缓存数据的话,缓存只需要删除一次

第三个问题,是先删除缓存然后操作数据库,还是先操作数据库再删缓存

因为先删除缓存,再操作数据库,出现第一种情况的时候,就算线程1更新了数据库,但是缓存中的内容仍旧是旧数据,在下一次更新缓存之前,可能会有无数线程获取到这个旧数据。

而先操作数据库,再删除缓存,出现第二种情况,就算线程1未命中缓存,查询到了数据库未更新前的旧数据,并将旧数据写入缓存中,但在线程2先更新数据库再删除redis的操作后,后面线程获取到的缓存,是新的数据。所以选择先操作数据库再删除缓存的策略。

解决redis缓存穿透

用缓存空对象的方法解决

解决redis缓存雪崩

用给不同的key的TTL添加随机值解决

解决redis缓存击穿

缓存击穿,如果热点数据未命中,在第一个重建缓存的线程还没完成更新的时候,又有大量线程访问去重建缓存,这个时候就会有大量请求打到数据库

在缓存击穿的第一种方法里,如果查询数据库并更新缓存数据的业务耗时比较长,那么后面如果有大量线程访问,则都会阻塞,影响性能。第二种方法的话,就是热点数据不设置过期时间,也就是永久有效,然后在value值里加一个逻辑过期时间,可以在活动结束以后删除这个缓存热点数据,许多线程获取这个缓存数据,然后线程看逻辑过期时间到期了没有,如果到期了,也就是缓存数据旧了,要更新缓存数据,第一个发现的线程加个互斥锁,不让其他线程一起查询数据库重建缓存数据,加了互斥锁以后,这个时候线程1直接创建另一个线程,让它完成缓存数据的重建,然后线程1自己继续使用以前的旧数据,如果这个时候有其他线程过来,发现缓存数据逻辑时间过期了,想要重建缓存数据,但是发现已经有线程加了互斥锁,这个时候就会直接拿缓存数据的旧数据返回

采用互斥锁来防止缓存击穿,这里不用synchronized锁,要自定义实现缓存击穿互斥锁逻辑,可以借助redis数据库里的String类型的某个命令,setnx,这个命令和互斥锁的原理相似,当某个key值不存在,则可以存储key-value(获取锁),当key值存在,采用setnx想更新value,这个时候不允许,因为key已经存在(锁已经被占用了),如果要释放锁,直接删除key-value就好。还要加一个有效期,防止出现某种问题,线程没有释放锁,当到达有效期后,锁会自动释放,就不用一直等待。

封装Redis的工具类

总结

1、为了提高查询速度,我们将需要数据放入了redis缓存当中

2、为了让数据库和reids中的数据保持一致,我们在数据库更新的时候,同时更新redis缓存 ,这里的更新,指的不是数据库的数据变一下,redis的缓存数据也变一下,而是直接删除redis的这个缓存数据,等到下一次需要用到这个数据,自然就会存入redis缓存当中。要先操作数据库,再更新删除redis,这两个操作必须一起执行,要么都成功,要么都失败,所以要用事务。

3、为了不让外界采用大量不存在的值让许多请求穿过redis缓存打到数据库上,解决redis的穿透问题,采用了缓存空对象null的解决方法。

4、为了不让redis中的缓存在某一时刻大量key失效,导致许多请求穿过redis缓存打到数据库上,解决redis的雪崩问题,采用了给不同的key的TTL添加随机值,来让它们不再同一时间内大量失效。

5、为了不让redis中的热点数据失效,然后在redis热点数据缓存还没有重建的时候,大量线程请求访问穿透redis缓存打到数据库上,解决缓存击穿的问题,采用了逻辑过期时间+分布式锁来解决。

 优惠券秒杀功能

全局ID生成器(不能使用数据库自增ID)

id的规律性太明显:用户可以通过id的规律猜测出一些信息,比如这个商家的优惠券被抢了多少张等等

单表数据无法承受常年累月积攒下来的订单数量,这个时候如果开辟多张表存储订单,则会让订单id相同,失去唯一性

用redis来实现全局ID生成器可以满足以下的特性。唯一性:redis本身就需要保证key值的唯一性。高可用:在面对一系列故障的时候,redis可以通过自身的机制如主从复制、哨兵、集群、持久化来保持持续的稳定进行。高性能:redis的数据存储在内存当中,高性能。递增性:redis的String类型的key有自增函数。安全性:为了保证ID的安全性,我们不能直接使用redis自增的数值,而是要拼接一些其他信息

 redis的String类型中的key占据8个字节,一个字节8个bit,也就是说key总共占据64位。

 时间戳:从某个时间点开始,比如2022年10月1日几时几分几秒,然后你下单的时间减去这个时间点,为了防止同一时刻多个用户下单,时间戳一样,后面32bit序列号作为区分,也就是redis的自增数值,每次调用自增1,理论上支持每秒产生2^32个不同的ID

 实现优惠券秒杀的下单功能

简单地实现订单下单的功能,在多线程并发请求的情况下,可能会出现超卖的请求,也就是说本来有一百份优惠券供抢,但是在多线程并发的请求下,出现了被抢走超出一百份的情况

更新数据时去判断有没有其他线程对数据做了修改

 上面这个方法可以简化为判断当前查询的库存是否和之前查询到的库存一致,以此来判断之前查询到的数据是否被修改过,但是如果仅仅只使用这种方法,则会导致失败率大大提高,也就是说有200个线程要去购买优惠券,后来只卖出了20张优惠券,其他180个线程都出错,明明还有80张优惠券可以买,线程却没买。因为这种方案下线程太小心了,只要判断到和自己上次看到的库存不一样,就认为出现了安全问题,就默认失败了,在优惠券大于0 的情况下,虽然产生了并发修改,但是并没有产生业务安全,而线程会误认为产生了业务安全。对于当前的库存问题,只需要将判断库存与之前查询到的库存数据是否相等,修改为判断库存是否大于0,毕竟只要大于0,就可以扣减库存

总结

 1、每下单一张优惠券,就会生成一个订单信息,这里的订单信息必须是唯一的,所以使用符号位+时间戳+序列号来生成全局ID,保证订单信息id的唯一性。

2、为了在多并发请求的情况下实现优惠券秒杀,防止超卖,采用锁的方式,为了保证性能,采用乐观锁,在更新数据的时候去判断有没有线程对数据做了修改,再决定要不要更新数据。

一人一单

实现这个业务以后,同样会和优惠券秒杀一样出现超卖的问题,因为不像上面那样是更新问题使用乐观锁(写多用乐观锁),这里是判断是否存在问题,使用悲观锁synchronized(读多用悲观锁,避免乐观锁的重试开销)

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

在解决一人一单问题的时候使用了悲观锁synchronized(只能保证单个jvm内部的多个线程互斥,而不能保证多个jvm内部的多个线程互斥),只能适用在一个服务器的情况下,如果是集群,多个服务器,那么这个程序就会出错。在两个服务器,如果同时同一个用户下单,这两个服务器的两个请求会同时获取到锁,然后同时进行操作。

在jvm维护了一个锁的监视器,当线程一获取锁的时候,监视器会记录线程一的名称,当线程二来获取的时候因为监视器已经记录了其他线程,所以获取失败了。服务器有独立的jvm,有两个服务器就有两个jvm,此时他们是不互通的,这个时候在第二台服务器,锁监视器是空的,线程3也可以在第一台服务器的线程1获取锁的情况下获取到锁

解决办法:让多个jvm使用同一个锁,也就是用分布式锁

 mysql通过事务,进行写操作的时候自动加锁

redis通过setnx,同一个key,只有一个线程能拥有这个锁,当释放锁后就删除key-value,然后其他线程就可以拥有这个锁

Zookeeper:有很多个节点,节点具有唯一性,不能重复,可以利用这个特性实现分布式锁,但是更多的是使用它的有序性,多个线程创建了多个节点,这些节点都有排序,默认让节点顺序最小的线程获取到锁, 释放锁后删除节点,最小节点更新,其他线程获取到锁

为了确保互斥,所以使用redis的setnx。为了防止加锁后线程宕机无法释放锁,主动给锁添加过期时间限制。为了保证原子性,也就是加锁和设置超时时间全都成功(不要出现,只加了锁,然后在设置超时时间的时候宕机了,导致没有设置超时时间),需要让加锁和设置超时时间一起执行,不能分成两条命令。为了提高性能,不采用阻塞式(获取锁失败一直等待,直到锁获取成功),才用阻塞式,只要获取失败就返回false不再等待。

如果分布式锁设置的过期时间把控不正确,设置时间不足以让线程1执行完业务,导致锁释放的时候线程1还在执行,而这时候线程2获取到了锁,在线程2执行业务的期间,线程1执行完业务了,它要去释放锁,直接删除了线程2持有的锁,然后线程3获得了锁,就导致了线程2和线程3的业务又开始并行执行了。

解决这个问题需要在释放锁前,看看这个锁是否是自己持有的,在value里记录了每个持有锁的线程的信息,这个线程的标志信息不能使用线程的id,因为线程的id是jvm内部维护的一个递增的数字,在集群的模式下,多个jvm内维护的递增数字很有可能冲突,就还有可能导致因为线程id一样而误释放锁的情况。这个时候使用uuid+线程id来作为线程的唯一标识,在线程去释放锁的时候,先去判断这个锁的持有线程的标识是否和自己的uuid+线程id的标识一致,如果一致,允许释放锁,不一致不释放锁

即使实现了唯一线程标识,仍旧可能出现多线程并行执行的情况,在线程1获取到锁,并且执行完业务要去释放锁,此时它已经判断好自己就是当前持有锁的线程,有资格释放锁,但是在释放锁之前,因为jvm的垃圾回收机制,此时阻塞了线程1释放锁的业务,然后这个时候锁的释放时间超时了,然后锁自动释放,线程2拿到了锁,然后线程1阻塞结束,又去执行释放锁业务,因为之前已经完成了判断,线程1仍旧以为这个锁是自己持有,然后去释放线程2的锁,这个时候线程3趁虚而入,又和线程2的业务并发执行了

 解决办法:必须保证判断锁和释放锁的业务一起执行,保证它们的原子性操作,这个时候使用redis的Lua脚本,把多条命令写在Lua脚本里,一起执行,保证原子性

就算这样实现了分布式锁,这个分布式锁中还存在着问题

 自己实现太繁琐,可以借助已经成熟的框架,直接使用

redisson的使用

 

Redisson可重入锁原理

和reentrantLock锁类似,依靠线程标识来判断当前获取锁的线程是不是自己,如果是自己,则允许再次获取锁,把获取锁的记录加1(这个时候不再像之前那样用redis的String类型存储key-value,而是用hashmap,因为我们不仅要存储线程唯一标识,还要存储线程重复获取锁的次数),因为获取锁释放锁的步骤很多,为了保证原子性,必须要用Lua脚本实现。

 

Redisson的锁重试和WatchDog机制

获取到锁以后,如果没有设置过期时间,则会自动开启看门狗机制,在业务未执行完前不断续期锁有效时间。获取锁失败以后,不是直接返回false,而是判断剩余等待时间,如果剩余等待时间不为0,则订阅释放锁的消息,在订阅的过程中,如果等待时间结束,直接返回false,如果等待时间没有结果,并且等到了锁释放消息,则再一次尝试获取锁。

Redisson的multiLock原理,解决主从一致问题

为了防止redis宕机以后影响业务,设置了主节点和若干从节点,一般来说,主节点负责写,从节点负责读,当在主节点获取到锁以后,主节点要把锁信息同步到从节点,还未同步就宕机了,这个时候由于redis的哨兵机制,会从从节点选择一个当主节点,但是这个新的主节点没有之前的锁数据,如果这个时候有其他线程来获取锁,就和之前的锁冲突了。

 这个问题是由于主节点无法同步到从节点产生的,那干脆就不要从节点,全都设为主节点,加锁的时候所有的主节点都需要同步锁信息,只有多数主节点都加锁成功,那才算加锁成功。即使我们又在主节点设置了若干从节点,一个主节点宕机,它的从节点接手,然后有线程获取锁,因为从节点成为主节点后没有之前的锁信息,所以可以获取锁成功,但是因为在多个主节点要服从少数服从多数原则,得要多个主节点加锁成功才算,因为其他主节点保存着之前的加锁信息,所以这次加锁仍旧会失败。

总结

1、自己自定义分布式锁解决一人下一单问题:setnx和设置过期时间在同一条语句中,保证原子性+用uuid+线程id建立线程唯一标识作为value值,防止误释放锁+用Lua脚本保证判断锁的所属线程和释放锁操作的原子性

2、自定义的分布式锁的不足:可重入锁、锁重试、看门狗机制、主从一致等问题用已经成熟的框架redisson解决

异步秒杀来实现:优惠券秒杀包含扣减库存+一人一单

两者都有超卖的风险,扣减库存的超卖用乐观锁解决,每次扣减时同时判断库存是否大于0,一人一单则使用悲观锁,锁住同一个user的id就好,防止同一个用户重复下单。

整个流程串行执行,性能不好,所以要优化

不要单线程串行执行,而是分成两个线程,分别完成各自的任务,这样就提高了并发性能

 要在redis完成判断秒杀库存和校验一人一单,首先要思考采用什么数据结构进行存储,库存只需要最简单的string类型。而一人一单,则是用不可重复value的有序集合来进行存储,这样只需要看看这个请求下单的用户的id是否在列表当中,如果在就代表已经下过单了,如果不在,并且库存充足,则可开启异步线程下单处理。整个判断过程要保证原子性,所以都需要在Lua脚本中进行。注意在判断可下单的同时还需要同步扣减redis当中的库存数量。

 

以上实现以后还会出现问题,问题一:jvm的内存是有限制的,如果阻塞队列满了以后还有下单请求到达怎么办。问题二:如果突然宕机了,内存里的数据都丢失了怎么办?线程还没执行完下单请求就宕机了怎么办?

这个时候就需要用到消息队列

Redis实现消息队列

消息队列是jvm以外的一个独立的服务,不受jvm内存的限制。消息队列也不仅仅是数据存储,还要确保我们的数据安全,也就是说,和阻塞队列不一样,存进消息队列里面的数据需要进行持久化,这样就能确保数据不会丢失。而且消息队列在吧消息投递给消费者以后还要消费者进行确认,如果消费者没有确认,则这个消息还会继续存储在消息队列当中,下次再次投递给消费者处理,直到成功位置,也就是确保消息至少被消费一次。

List结构实现消息队列

缺点解释:无法避免消息丢失:一旦消息被消费者拿去,如果这个时候消费者还没执行完任务,然后宕机了,这个时候消息队列并不会有复原消息的功能,丢了就是丢了。

只支持单消费者:被一个消费者拿走消息后,其他消费者无法获得消息。

PubSub实现消息队列

消费者那会有一个缓冲区来接收消息,如果超出这个缓冲区的内存,那些消息就会被丢弃

基于Stream的消息队列

Stream是redis中的一种数据结构,和字符串,集合的那种数据结构一样,保障了数据的持久化

持续监听队列,但是会出现漏读现象,因为$只读最新的消息,如果一下子发来好多条消息,只能读到最后的也就是最新的一条消息

消息可回溯:消息一直保存,后面可随时取用

http://www.dtcms.com/a/474344.html

相关文章:

  • C++11(列表初始化、右值引用和移动语义)
  • 北京规划建设 杂志 官方网站多说与网站账号绑定
  • 网站建站外包公司产品做网站如何谁来维护价格
  • 电子商务网站开发文档济宁seo优化公司
  • 提效工具推荐-任务关系和状态自动转为 UML图
  • 集合(Set)的使用场景与习惯养成指南
  • 【每日一题】3186. 施咒的最大总伤害
  • 如何做新网站php 资讯网站
  • 专门做推广的网站吗哪个网站可以改字体
  • 小迪安全v2023学习笔记(一百零三讲)—— 漏扫项目篇PoC开发Rule语法反链判断不回显检测Yaml生成
  • MySQL的练习题二----创建表的练习题
  • 设备技术支持东莞网站建设大连模板网站制作多少钱
  • Dockerfile 指令详解与实战指南
  • 10.1 快速排序(排序(下))
  • 前端梳理体系从常问问题去完善-框架篇(Vue2Vue3)
  • 【数据分享】青藏高原10米分辨率DEM数据集
  • 网站正在建设中空白模板网站建设调查通知
  • 【星闪】Hi2821 | 低功耗开发 + 低功耗管理及按键唤醒例程
  • 怎么做网站的网盘品牌网站建设公司有哪些
  • 【Linux】Socket编程基础
  • 长春建网站wordpress 图片打开慢
  • 91工业设计网模板网站有利于做seo吗
  • 做网站蓝色和什么颜色搭配好看万维网站注册
  • Windows 10 使用 VMware Workstation 搭建 Ubuntu 虚拟机
  • 深入解析Litho的多智能体协同架构与ReAct推理机制
  • 机器视觉---ViBe算法
  • Product Hunt 每日热榜 | 2025-10-12
  • C++11 多线程与并发编程
  • 太原网站建设优化有什么ae做动图的网站
  • 【全志V821_FoxPi】9-3 Linux IIC驱动SSD1306(0.96寸oled屏幕)