【黑马点评】面试可能被问整理

短信登录

具体代码实现可以看一下我以前做的笔记:黑马点评 短信登录部分

基于session实现登录流程

1.发送验证码

用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号

如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

2.短信验证码登录、注册用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息

3.校验登录状态

用户在请求的时候,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并放行

这段话的大概意思是什么呢:用户请求的时候,会从cookie中携带JsessionId到后台,就如图一样去看这个键值对,键=值,根据这个值去查找这个用户信息,没有这个session信息,就拦截,有就将用户信息保存到threadLocal里面,放行

ThreadLocal可以解释成线程的局部变量,也就是说一个ThreadLocal的变量只有当前自身线程可以访问,别的线程都访问不了

我们从浏览器输入框键入网址,首先浏览器做的第一步工作就是要对 URL 进行解析,从而生成发送给 Web 服务器的请求信息。服务器就帮我去请求这个信息,就是先去问dns

(dns域名解析:看图

首先本地电脑会检查浏览器缓存中有没有这个域名对应的解析过的IP地址,如果缓存中有,这个解析过程就结束。 如果浏览器缓存中没有数据,浏览器会查找操作系统缓存中是否有这个域名对应的DNS解析结果。前两个过程无法解析时,就要用到我们网络配置中的"DNS服务器地址"了。操作系统会把这个域名发送给这个本地DNS服务器。如果本地DNS服务器仍然没有命中,就直接到根DNS服务器请求解析。根DNS服务器返回给本地DNS域名服务器一个顶级DNS服务器地址,它是国际顶级域名服务器,如.com、.cn、.org等, 本地DNS服务器再向上一步获得的顶级DNS服务器发送解析请求。诶没有顶级DNS服务器就让他去问权威DNS服务器,权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。)

通过 DNS 获取到 IP 后,就可以把 HTTP 的传输工作交给操作系统中的协议栈。

http协议是处于TCP/IP协议的体系的应用层,它属于应用层的协议

session的数据是存储于服务器端的,服务端的数据量非常大的时候,就容易造成内存不足

redis是基于内存的高性能数据库,他的读写速率非常快和session相似

Cookie的工作原理

(1)浏览器端第一次发送请求到服务器端

(2)服务器端创建Cookie,该Cookie中包含用户的信息,然后将该Cookie发送到浏览器端

(3)浏览器端再次访问服务器端时会携带服务器端创建的Cookie

(4)服务器端通过Cookie中携带的数据区分不同的用户

Session的工作原理

(1)浏览器端第一次发送请求到服务器端,服务器端创建一个Session,同时会创建一个特殊的Cookie(name为JSESSIONID的固定值,value为session对象的ID),然后将该Cookie发送至浏览器端

(2)浏览器端发送第N(N>1)次请求到服务器端,浏览器端访问服务器端时就会携带该name为JSESSIONID的Cookie对象

(3)服务器端根据name为JSESSIONID的Cookie的value(sessionId),去查询Session对象,从而区分不同用户。

name为JSESSIONID的Cookie不存在(关闭或更换浏览器),返回1中重新去创建Session与特殊的Cookie

name为JSESSIONID的Cookie存在,根据value中的SessionId去寻找session对象

value为SessionId不存在**(Session对象默认存活30分钟)**,返回1中重新去创建Session与特殊的Cookie

value为SessionId存在,返回session对象

DTO和VO

DTO

接受前端数据传输给service层

service层传输数据给前端数据

中间就是要通过cotrooer层

用于服务层之间数据传输。

包含数据对象,主要是变量定义和get、set方法。

dto可以封装需要传输的数据。

VO

在controller层将数据传递给前端展示。

fillBeanWithMap的意思就是这个

我们想redis存入的是hash

如果我们要将信息存入ThreadLocal中的话,那么就要将map转为bean

Session集群共享问题

如何解决集群的session共享问题?

用redis替代session实现登录注册功能啊,分布式系统下每一个服务器的session都是独立的,就是说你切换到其他服务器,比如你要切换到发布帖子,查看评论什么的,再次调回来登录界面又要重新登录了,多次重复登录多麻烦啊,这得使用redis可以保证多个服务器访问的是同一个redis,则保证了不会再重复登录了,实现了数据的共享;

为什么redis能解决集群的session共享问题?

先说一下Session集群共享问题造成哪些问题?

比如:在当前这个服务器上用户已经完成了登录,Session中存储了用户的信息,能够判断用户已登录,但是在另一个服务器的Session中没有用户信息,无法调用显示没有登录的服务器上的服务

为什么会出现这种情况??

如图:

拿session来说,一个用户它去B服务器登录注册,B服务器上保存了它的信息,此时正在向A,C服务器同步信息,但是此时诶,浏览器关了或者跑到其他页面去了,回来到C服务器登录注册页面的时候,发现没有登录成功还要再重复登录一次。因为B服务器并没有成功的向A,C服务器同步用户的信息。

这个时候需要redis替代session实现登录注册功能

就前面校验登录状态:(用户在请求的时候,会从cookie中携带JsessionId到后台,去看那个键值对,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截(就是登录不成功),如果有session信息,则将用户信息保存到threadLocal中,并放行(登录成功))这个原理来说,使用redis可以保证多个服务器访问的是同一个redis(因为用户在B服务器上保存的信息其实已经保存在redis里面了,其他服务器都可以来访问redis里面的信息,来查找session信息,看有没有他们想要的信息,大概意思就是这样)

Redis缓存相较于传统Session存储的优点:(记一下就行)

高性能和可伸缩性:Redis 是一个内存数据库,具有快速的读写能力。相比于传统的 Session 存储方式,将会话数据存储在 Redis 中可以大大提高读写速度和处理能力。此外,Redis 还支持集群和分片技术,可以实现水平扩展,处理大规模的并发请求。

可靠性和持久性:Redis 提供了持久化机制,可以将内存中的数据定期或异步地写入磁盘,以保证数据的持久性。这样即使发生服务器崩溃或重启,会话数据也可以被恢复。

丰富的数据结构:Redis 不仅仅是一个键值存储数据库,它还支持多种数据结构,如字符串、列表、哈希、集合和有序集合等。这些数据结构的灵活性使得可以更方便地存储和操作复杂的会话数据。

分布式缓存功能:Redis 作为一个高效的缓存解决方案,可以用于缓存会话数据,减轻后端服务器的负载。与传统的 Session 存储方式相比,使用 Redis 缓存会话数据可以大幅提高系统的性能和可扩展性。

可用性和可部署性:Redis 是一个强大而成熟的开源工具,有丰富的社区支持和活跃的开发者社区。它可以轻松地与各种编程语言和框架集成,并且可以在多个操作系统上运行。

双重拦截器

配置登录拦截器:

原理就是上面 的“3.校验登录状态”

还需要单独配置一个拦截器用户刷新Redis中的token:

我们之前只配置了一个登录拦截器,这里需要另外再配置一个拦截器专门用户刷新存入Redis中的token,因为我们现在改用Redis了,为了防止用户在操作网站时突然由于Redis中的 token 过期,导致直接退出网站,严重影响用户体验。那为什么不把刷新的操作放到一个拦截器中呢,因为之前的那个拦截器只是用来拦截一些需要进行登录校验的请求,对于哪些不需要登录校验的请求是不会走拦截器的,刷新操作显然是要针对所有请求比较合理,所以单独创建一个拦截器拦截一切请求,刷新Redis中的Key

然后将自定义的拦截器添加到SpringMVC的拦截器表中,使其生效

啊大概先到这了,后续有补充的会继续补充上去。

商户查询缓存

 什么是缓存?

缓存就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地

怎么说呢,为什么要用缓存这种东西,就是为了用户带着大量的请求来请求数据,如果没有缓存,那么就会直接去攻击数据库,去数据库里面请求数据,给数据库带来巨大的压力

为什么要用缓存?

速度快,好用

Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。

缓存更新策略

缓存更新是Redis为了节约内存而设计出来的一种东西,就是这个redis内存数据宝贵,往他里面插入的数据太多了,就会导致缓存中数据过多,所以redis会对部分数据进行更新,或者把它淘汰

内存淘汰:Redis自动进行,当Redis内存大道我们设定的max-memery时,会自动触发淘汰机制,淘汰掉一些不重要的数据

主动更新:直接手动调用方法把缓存删除掉,通常用于解决缓存和数据库不一致问题

超时剔除:给redis设置过期时间之后,过期了的 超时了的数据给他删除掉

数据库和缓存如何保证一致性?

参考

数据库和缓存如何保证一致性? | 小林coding (xiaolincoding.com)

先更新数据库还是新更新缓存的问题?

不管是先更新数据库还是先更新缓存,都会导致缓存和数据库数据不一致的问题,所以直接不考虑

所以来到先更新数据库还是新删除缓存?

1 先删除缓存再更新数据库(旁路缓存机制)

举个例子,用户A 先去缓存删除了20,然后用户B来缓存里查找20没查找到,然后它就去数据库里读取数据20,并写入缓存里,用户A才去数据库里面更新了数据为21,此时缓存里数据是20,有缓存和数据库数据不一致的问题

针对先删除缓存再更新数据库造成缓存和数据库数据不一致的问题的解决方案:延迟双删

#删除缓存

redis.delKey(X)

#更新数据库

db.update(X)

#睡眠

Thread.sleep(N)

#再删除缓存

redis.delKey(X)

2 先更新数据库再删除缓存

假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中,最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库数据不一致。

先更新数据库,再删除缓存也会出现缓存和数据库数据不一致的问题,但是!问题出现的概率不高,因为redis它读写速度很快,性能高,所以缓存的写入远远快于数据库的写入

「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

什么是脑裂?

在 Redis 主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。 如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。

这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了。

然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。

总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

解决方案

当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。

在 Redis 的配置文件中有两个参数我们可以设置:

min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。

min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。

我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。

这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。

即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。

等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。

旁路缓存机制:

决定在更新数据时,先更新数据库中的数据,再删除缓存中的数据,然后到读取数据后,发现缓存中没有了数据,再从数据库中读取数据,更新到缓存当中。

可以看一下我以前做项目时候写的这篇文章:Redis的缓存问题|将redis常用操作封装成工具类

缓存更新方法封装 用到了泛型、函数式编程。

使用函数式编程是因为我们这个是一个通用的工具,使用泛型(泛型(Generics) 允许我们定义类、接口和方法,可以使用不同类型的参数进行操作)可以实现数据类型的通用性,但是对于不同的数据要查询的数据库不同,查询的逻辑也不同,所以工具不方便封装。可以将查询数据库的部分交给调用者实现,传参的时候传一个Function接口,我们这个工具直接调用接口里面的方法就可以了

函数式编程

函数式编程 - 廖雪峰的官方网站

JavaLearningRecord/黑马点评.md at main · CSfreshman/JavaLearningRecord · GitHub

函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y

= sin(x),x 和 y 的关系

相同的输入始终要得到相同的输出(纯函数) * 函数式编程用来描述数据(函数)之间的映射

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

深入理解函数式编程(上) - 美团技术团队

函数式编程是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算是该语言最重要的基础。而且λ演算的函数可以接受函数作为输入的参数和输出的返回值。

我们可以直接读出以下信息:

避免状态变更

函数作为输入输出

和λ演算有关

深入理解函数式编程(上) - 美团技术团队

缓存穿透,缓存击穿

Redis 常见面试题 | 小林coding (xiaolincoding.com)

缓存雪崩

当大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

解决方案:

将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。

设置缓存不过期: 我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。

缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

解决方案:

互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

缓存穿透

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

解决方案:

缓存穿透的发生一般有这两种情况:

业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;

黑客恶意攻击,故意大量访问某些读取不存在数据的业务;

应对缓存穿透的方案

非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

优惠券秒杀

乐观锁:

乐观锁认为线程安全问题不一定会发生,因此不加锁,只是在更新数据的时候再去判断有没有其他线程对数据进行了修改

如果没有修改,则认为自己是安全的,自己才可以更新数据

如果已经被其他线程修改,则说明发生了安全问题,此时可以重试或者异常

乐观锁解决商品库存超卖问题

超卖问题:(多线程安全问题)就是明明已经卖完了,居然还能卖出去-1件

假设现在只剩下一张优惠券,线程1 过来查询库存,发现库存数大于1,还没来得及去扣减库存,此时库线程2也过来查询库存,发现库存数也大于1,那么这两个线程都会进行扣减库存操作,最终相当于是多个线程都进行了扣减库存,那么此时就会出现超卖问题

乐观锁:乐观锁会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如CAS

乐观锁的典型代表:就是CAS(Compare-And-Swap比较并替换),利用CAS进行无锁化机制加锁,var5 是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值

具体解决方式:

使用stock(库存)来充当版本号,在扣减库存时,比较查询到的优惠券库存和实际数据库中优惠券是否相同(这句话非常重要!)

看代码;

@Override

public Result seckillVoucher(Long voucherId) {

LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();

//1. 查询优惠券

queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);

SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);



//2. 判断秒杀时间是否开始

if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {

return Result.fail("秒杀还未开始,请耐心等待");

}

//3. 判断秒杀时间是否结束

if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {

return Result.fail("秒杀已经结束!");

}

//4. 判断库存是否充足

if (seckillVoucher.getStock() < 1) {

return Result.fail("优惠券已被抢光了哦,下次记得手速快点");

}

//5. 扣减库存

boolean success = seckillVoucherService.update()

.setSql("stock = stock - 1")

.eq("voucher_id", voucherId)

+ .eq("stock",seckillVoucher.getStock())

.update();

if (!success) {

return Result.fail("库存不足");

** }**

扣减库存这段代码的核心含义:

每下单一次,库存-1,只要我扣减库存时的库存和我之前查询到的库存一样的,就意味着没有人在中间修改过库存,那么此时是安全的,就是不会出现超卖的问题,但是如果没成功,那么意味着有人是在之前修改过库存的,那么就会出现超卖问题

但是以上这种方式通过测试发现会有很多失败的情况,失败的原因在于:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败

就是说:十个线程 刚开始的时候拿到库存为2,然后有其中一个线程扣减了一个库存,那就变成了1,然后剩下的九个库存检查发现库存已经背修改,so就修改失败

//6. 创建订单

VoucherOrder voucherOrder = new VoucherOrder();

//6.1 设置订单id

long orderId = redisIdWorker.nextId("order");

//6.2 设置用户id

Long id = UserHolder.getUser().getId();

//6.3 设置代金券id

voucherOrder.setVoucherId(voucherId);

voucherOrder.setId(orderId);

voucherOrder.setUserId(id);

//7. 将订单数据保存到表中

save(voucherOrder);

//8. 返回订单id

return Result.ok(orderId);

}

使用Redisson来实现一人一单功能

一人一单就是指同一个优惠券,一个用户只能抢一张

逻辑:判断库存是否充足之后,根据保存的订单数据,判断用户订单是否存在

存在就说明下过单,不能继续下单了,返回错误信息

不存在就说明没下过单,继续下单,获取优惠券

初始代码:

@Override

public Result seckillVoucher(Long voucherId) {

LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();

//1. 查询优惠券

queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);

SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);

//2. 判断秒杀时间是否开始

if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {

return Result.fail("秒杀还未开始,请耐心等待");

}

//3. 判断秒杀时间是否结束

if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {

return Result.fail("秒杀已经结束!");

}

//4. 判断库存是否充足

if (seckillVoucher.getStock() < 1) {

return Result.fail("优惠券已被抢光了哦,下次记得手速快点");

}

+ // 一人一单逻辑

+ Long userId = UserHolder.getUser().getId();

+ int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();

+ if (count > 0){

+ return Result.fail("你已经抢过优惠券了哦");

+ }

还是和之前一样,如果这个用户故意开多线程抢优惠券,那么在判断库存充足之后,执行一人一单逻辑之前,在这个区间如果进来了多个线程,还是可以抢多张优惠券的,(怎么说呢,就是在判断库存充足之后,执行一人一单逻辑之前,其实有多个线程是直接跳过一人一单逻辑来到扣减库存这步的,所以还是一个人能同时获取多张优惠券)那我们这里使用悲观锁来解决这个问题

//5. 扣减库存

boolean success = seckillVoucherService.update()

.setSql("stock = stock - 1")

.eq("voucher_id", voucherId)

.gt("stock", 0)

.update();

if (!success) {

return Result.fail("库存不足");

}

//6. 创建订单

VoucherOrder voucherOrder = new VoucherOrder();

//6.1 设置订单id

long orderId = redisIdWorker.nextId("order");

//6.2 设置用户id

Long id = UserHolder.getUser().getId();

//6.3 设置代金券id

voucherOrder.setVoucherId(voucherId);

voucherOrder.setId(orderId);

voucherOrder.setUserId(id);

//7. 将订单数据保存到表中

save(voucherOrder);

//8. 返回订单id

return Result.ok(orderId);

}

就你不管中间是有多少的问题解决方案的改进,都是存在问题的

尤其是到最后的分布式锁原子性问题:

看上图:更为极端的误删逻辑说明

假设线程1已经获取了锁,在判断标识一致(防止误删)之后,准备释放锁的时候,又出现了阻塞(例如JVM垃圾回收机制)

于是锁的TTL到期了,自动释放了

那么现在线程2趁虚而入,拿到了一把锁

但是线程1的逻辑还没执行完,那么线程1就会执行删除锁的逻辑

但是在阻塞前线程1已经判断了标识一致,所以现在线程1把线程2的锁给删了

(那么就相当于判断标识那行代码没有起到作用)

这就是删锁时的原子性问题

因为线程1的拿锁,判断标识,删锁,不是原子操作(就是不可分割的操作,保证在执行过程中不会被其他线程中断,这里线程2,线程3都乘虚而入了,当然不是原子操作),所以我们要防止刚刚的情况

Lua脚本解决多条命令原子性问题

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

Lua是一种编程语言,它的基本语法可以上菜鸟教程看看,链接:Lua 教程 | 菜鸟教程

这里重点介绍Redis提供的调用函数,我们可以使用Lua去操作Redis,而且还能保证它的原子性,这样就可以实现拿锁,判断标识,删锁是一个原子性动作了

MQ消息队列+Lua 脚本实现异步处理下单流程

 具体实现和代码可参考我以前做过的笔记:《黑马点评》异步秒杀优化|消息队列

回顾一下下单流程

用户发起请求

会先请求Nginx,Nginx反向代理到Tomcat,而Tomcat中的程序,会进行串行工作,

分为以下几个操作:

1 查询优惠券

2 判断秒杀库存是否足够

3 查询订单

4 校验是否是一人一单

5 扣减库存

6 创建订单

像什么一人一单,扣减库存这些地方都是要操作数据库的,所以时间会消耗比较多,效率比较低,就是说 ,他是一个串行执行的,所以就需要异步执行操作

优化方案:

我们将耗时较短的逻辑判断放到Redis中,例如:库存是否充足,是否一人一单这样的操作,只要满足这两条操作,那我们一定是可以下单成功的

其次不用等数据真的写进数据库,直接告诉用户我们下单成功就好了,后续的操作呢,后台直接再开一个线程,后台线程再去慢慢执行队列里的消息,这样就可以很快的完成下单任务了,就是先不管有没有真正的下单成功,只要点了下单优惠券就告诉用户下单成功就好了,后续的操作有点慢,得后台的线程再去慢慢的执行队列里的消息

但是存在一个问题就是:有可能已经告诉用户下单成功了,但后边出现了的问题导致其实并没有真正下单成功,那这张优惠券就用不了

这其中存在两个难点:

1 怎么在Redis中快速校验是否一人一单,还有库存判断

2 校验一人一单和将下单数据写入数据库,这是两个线程,我们怎么知道下单是否完成

整体思路:

当用户下单之后,判断库存是否充足,只需要取Redis中根据key找对应的value是否大于0即可,如果不充足,则直接结束。如果充足,则在Redis中判断用户是否可以下单,怎么判断呢?

用set集合,用户一旦下单就将用户id(userId)存入到集合当中,诶当下一次用户下单的时候,就判断一下userId是否在集合里面,如果set集合里面没有该用户的下单数据,就可以下单,如果里面有用户的userId,那就不能下单了,并将userId和优惠券存入到Redis当中,并且返回0,整个过程保证原子性的(也就是没有其他线程插入),所以需要用Lua来操作

同时由于我们需要在Redis中查询优惠券信息,所以在我们新增秒杀优惠券的同时,需要将优惠券信息保存到Redis中

看下图Lua脚本:

同步和异步的理解

同步是指:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。

异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。

eg:

比方说一个人边吃饭,边看手机,边说话,就是异步处理的方式。

同步处理就不一样了,说话后在吃饭,吃完饭后在看手机,必须等待上一件事完了,才执行后面的事情。

同步是阻塞模式,异步是非阻塞模式

同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;

同步就相当于是 当客户端发送请求给服务端,在等待服务端响应的请求时,客户端不做其他的事情。当服务端做完了才返回到客户端。这样的话客户端需要一直等待。用户使用起来会有不友好。

异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

异步就相当于当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情,这样节约了时间,提高了效率。

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值