黑马点评
1、使用双重拦截器完成用户认证和刷新登录态,提高用户的浏览体验。
登录:
登录的实现主要由三部分构成,分别是发送短信验证码、验证登录信息以及保持登录态和刷新登录态。
①发送短信验证码过程,这里的短信验证没有具体实现,因为实现需要使用第三方的接口调用短信服务的api实现登录。具体来说,首先用正则表达式校验手机号是否符合规则,如果不符合则返回错误信息;如果符合规则,那么使用Hutool下的RandomUtil工具类随机生成6位随机数字作为验证码;然后将验证码保存在Redis中,使用redis的String字符串数据结构来保存,因为保存的验证码要有唯一性,我们使用手机号作为key,并加了一个前缀来标识,最后设置了2分钟的过期时间,到达这个时间自动删除;最后发送验证码,返回成功信息。
②验证登录信息过程,校验手机号是否符合规则,如果不符合则返回错误信息;从 Redis 中获取该手机号对应的验证码,并判断验证码是否与用户输入的一致,如果不一致则返回错误信息;如果验证码一致,根据手机号查询用户是否存在;如果用户不存在,创建用户;将用户信息存储到 Redis 中;然后,随机生成一个 token,作为用户的登录令牌,将用户信息以 hash 的形式保存在 Redis 中,key 为 前缀+ token,value为用户信息(id、头像、昵称)。这里的 token 用了 UUID 随机生成全局唯一标识,避免 token 重复,还设置了一个有效期,不然就会一直挂在那里;最后,返回token。
③刷新登录态过程,这里使用了两个拦截器,第一个拦截器拦截一切路径,用以刷新token有效期,如果用户访问页面超过了 30 分钟,也是有操作的,如果不刷新 token,就会导致突然下线,这样用户体验不好(获取token、查询Redis的用户、保存到ThreadLocal、刷新token有效期、放行);第二个拦截器拦截登录路径,某些页面只有登录成功的用户才可以访问(查询ThreadLocal的用户、不存在则拦截,存在则放行)。
2、通过Redis缓存商铺数据提高用户查询速度,采用超时删除+主动更新方案保持数据一致性;
①缓存商户
商户信息缓存的实现是将商户信息存储到 Redis 中,下次查询时先从缓存中获取,如果缓存中存在数据则直接返回给用户,否则从数据库中查询并将结果存入缓存。
②缓存更新
我们项目中使用主动更新的双写方案来解决店铺信息缓存和数据库不一致问题,也就是当缓存调用者在更新完数据库之后再去更新缓存,我们这里是直接删除缓存的,因为更新缓存需要进行一些无效的写操作,而直接删除缓存快一些,还不容易出错。(先操作数据库,再删除缓存,尽量避免线程安全问题)
③延迟双删和主动更新的双写?为什么采用后者???
延迟双删,如果是写操作,要先把缓存中的数据删除,然后更新数据库,最后再延迟删除缓存中的数据。其中这个延时多久不太好确定,在延迟的过程中可能会出现脏数据,并没有保证强一致性,所以没有采用它。
3、通过缓存空值解决缓存穿透问题,减少数据库访问压力;
①缓存穿透问题
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效(只有数据库查到了,才会让 redis 缓存,但现在的问题是查不到),会频繁的去访问数据库。
我们这里使用缓存空对象的方式来解决缓存穿透问题,先去 Redis 中查询目标数据是否已经缓存,如果能查询到就直接返回缓存的数据;如果 Redis 中不存在目标数据,则去 MySQL 数据库中查询是否存在该数据;如果数据库中也查询不到,则将一个空字符串作为响应内容存入 Redis,设置短暂(一般 5 分钟以内)的过期时间,并返回错误信息;如果数据库中存在该数据,则将数据缓存到 Redis 中,并设置过期时间。
②缓存雪崩问题
缓存雪崩是指在同一时间段,大量缓存的 key 同时失效,或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。
-
给不同的 Key 的 TTL 添加随机值,让其在不同时间段分批失效
-
缓存数据永不过期:对于一些不会经常改变的数据,可以设置为永不过期,这样可以将这些数据作为一种缓存数据的备选方案,避免缓存雪崩导致数据不可用。
-
利用 Redis 集群提高服务的可用性(使用一个或者多个哨兵(Sentinel)实例组成的系统,对 redis 节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转移,保证系统的可用性。 )
-
给缓存业务添加降级限流策略
-
给业务添加多级缓存(浏览器访问静态资源时,优先读取浏览器本地缓存;访问非静态资源(ajax 查询数据)时,访问服务端;请求到达 Nginx 后,优先读取 Nginx 本地缓存;如果 Nginx 本地缓存未命中,则去直接查询 Redis(不经过 Tomcat);如果 Redis 查询未命中,则查询 Tomcat;请求进入 Tomcat 后,优先查询 JVM 进程缓存;如果 JVM 进程缓存未命中,则查询数据库)
4、通过给热点数据设置逻辑过期,解决缓存击穿问题;
①缓存击穿问题
缓存击穿也称为热点 Key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,那么无数请求访问就会在瞬间给数据库带来巨大的冲击
举个不太恰当的例子:一件秒杀中的商品的 key 突然失效了,大家都在疯狂抢购,那么这个瞬间就会有无数的请求访问去直接抵达数据库,从而造成缓存击穿
我们这里使用的设置逻辑过期时间来解决缓存击穿问题。当用户开始查询 redis 时,判断缓存是否命中;如果没有命中则直接返回空数据,不查询数据库;如果命中,则将 value 取出,判断缓存是否过期;如果没有过期,则直接返回 redis 中的数据;如果过期,则尝试获取互斥锁;如果获取锁失败,直接返回之前的数据;如果获取锁成功,在开启独立线程后,直接返回之前的数据,独立线程去重构数据并设置逻辑过期时间,重构完成后再释放互斥锁。
因为现在 redis 中存储的数据的 value 需要带上过期时间,我们选择新建一个实体类,包含原有数据(用万能的 Object)和过期时间,这样对原有的代码没有侵入性。
优惠卷秒杀:
优惠卷秒杀的逻辑
首先提交优惠券id;然后查询优惠券信息;之后判断秒杀时间是否开始,如果没有开始,返回异常信息;如果开始了,判断库存是否充足,如果库存不充足,也返回异常信息;如果库存充足,那么进行删减库存的操作;最后创建订单,创建成功后返回订单id。
这里并没有使用数据库的自增ID作为优惠券的ID(因为ID规律性太明显,容易让别人猜出我们的一些敏感信息,例如商城一天之内卖出多少单),我们使用全局ID生成器来保证ID的唯一性,ID的组成是符号位、时间戳、序列号的拼接。
5、使用乐观锁解决商品超卖问题,,解决并发情况下造成的安全问题;
①聊聊你的乐观锁。(超卖问题)
悲观锁认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行
乐观锁认为线程安全问题不一定会发生,因此不加锁,只是在提交回数据的时候再去判断有没有其他线程对数据进行了修改。因为,乐观锁会有一个版本号,每次操作数据会对版本号+1,在提交回数据时,会去校验是否比之前的版本大 1 ,如果大 1 ,则操作成功,也就是说,如果在操作过程中,如果版本号只比原来大 1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大 1,则数据被修改过,此时可以重试或者异常
我们并没有真的指定版本号,而是使用 stock 来充当版本号,在扣减库存时,只要比较查询到的优惠券库存和实际数据库中的优惠券库存是否相等。但是也要考虑并发问题,所以我们最终只判断是否有剩余优惠券,即只要数据库中的库存大于 0,就都能顺利完成扣减库存的操作。
②
我们在判断库存是否充足之后,根据我们保存的订单信息,判断用户订单是否已经存在。如果已经存在,则不能下单,返回错误信息;如果不存在,则继续下单,获取到优惠券。考虑到在判断库存是否充足之后,执行一人一单逻辑之前,某个用户故意开多线程抢优惠券的情况,我们使用悲观锁解决。我们给一人一单逻辑之后的代码加上synchronized关键字。不管哪一个线程,在运行到这个方法时,都要检查是否有其它线程正在用这个方法,如果有的话要等正在使用 synchronized 方法的线程运行完这个方法后再运行当前线程 ,如果没有其他线程的话,则锁定调用者,然后直接运行后续一人一单代码。
6、通过Redis使用Stream数据结构作为消息队列再结合Lua脚本实现异步秒杀功能,平衡后端服务的负载;
我们把查询优惠券、查询订单、删减库存和创建订单放到MySQL中,把判断秒杀库存和校验一人一单放到Redis中。也就是说,我们将耗时较短的逻辑判断放到 Redis 中,例如:库存是否充足,是否一人一单这样的操作,只要满足这两个操作,那用户是一定可以下单成功的,不用等数据真的写进数据库,我们直接告诉用户下单成功就好了。然后,后台再开一个线程,后台线程再去慢慢执行队列里的消息,这样我们就能很快完成下单业务。
第一步,创建一个Stream类型的消息队列,名为stream.orders。第二步,当用户下单之后,判断库存是否充足,如果不充足,则直接返回。如果充足,则在 Redis 中判断用户是否可以下单,如果 redis的set 集合中没有该用户的下单数据,则可以扣减库存,并将 userId 存入当前优惠券的set集合,再将下单数据保存到消息队列stream.orders中(内容包含voucherId、userId、orderId),并且返回 0,整个过程需要保证是原子性的,所以我们要用 Lua脚本来操作。我们规定,在Lua脚本中,库存不充足返回1,用户已经下单过,返回2。同时由于我们需要在 Redis 中查询优惠券信息,所以在我们新增秒杀优惠券的同时,需要将优惠券信息保存到 Redis 中。当完成以上逻辑判断时,我们只需要判断当前 Redis 中的返回值是否为 0(因为使用的stringRedisTemplate执行的Lua脚本),如果是 0,则表示可以下单。第三步,项目启动时,再开启一个线程任务,尝试获取消息队列stream.orders中的消息,完成下单。
下单具体步骤:
-
使用 RedisTemplate 的 opsForStream 方法从队列中读取一条消息。
-
判断读取的消息是否为空,若为空,则继续循环等待下一条消息。
-
若不为空,将读取的消息转换为 VoucherOrder 对象。
-
执行下单逻辑,并将数据保存到数据库中。
-
手动 ACK,确认当前处理的消息已经被处理完成
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器