谷粒商城项目总结
一、商品上架
1、为什么上架的商品信息不存到数据库?
答:商品上架是将后台的商品放在ES中,为用户提供全文检索功能,因此这个问题就围绕MySQL与ES在全文检索中的优缺点去进行阐述。
MySQL | Lucene | |
---|---|---|
磁盘IO层面 | Mysql只有term dictionary这一层,以B+树的数据结构存储在磁盘中,由于数据存储在B+树的叶子节点中,因此检索一个term词项需要若干次的随机磁盘IO,代价昂贵 | Lucene底层基于倒排索引技术,在term dictionary的基础上添加了term index来加速检索,term index以树的形式(FST)缓存在内存中。首先在内存term index中查到对应的term dictionary的磁盘块block位置之后,再去磁盘上找term,大大减少了磁盘随机IO的次数; |
数据结构层面 | B+树(存储在磁盘) | FST(存储在内存),一种变种字典树,不但能共享前缀还能共享后缀;不但能判断查找的key是否存在,还能给出响应的输出(posting list);他在时间复杂度与空间复杂度上都做到了最大程度的优化,使得Luence能够将term dictionary的信息完全加载到内存,以根据读内存快速的定位到term所在的磁盘block位置; |
磁盘存储层面 | 磁盘页(16KB) | 以磁盘块分block的形式进行存储,每个block内部利用公共前缀压缩,可以比b+tree更加节约磁盘空间 |
读写层面 | 多次随机磁盘IO,而且为了保证数据安全,需要加锁 | 先读内存定位后,再去磁盘找,IO次数少。同时ES底层存储数据的结构是Segment段对象,不可变,不需要加锁且并发性能好。 |
应用层面 | ① 使用MySQL进行全文检索时,很容易因为左前缀原则等造成索引失效; ②使用MySQL进行全文检索的查询结果 相关性差 ,并且无法与其它属性相关联; | 倒排索引技术,更快的查询,且能够通过倒排表posting list进行排序打分,能够给出相关性; |
2、上架细节
-
(1)将商品sku字段信息封装成mapping信息,在ES中建立product索引。
步骤 描述 Step1 客户端发送写数据的请求,ES为数据分配一个文档ID;请求可以随机发往任意节点,该节点成为协调节点; Step2 根据hash算法或者轮询法,计算文档要写入哪个分片(sharding); Step3 协调节点进行路由,将写数据请求转发给shard_ID所在的ES节点; Step4 (先写内存)
在内存中建立一个索引 (Index),该Index在内存中会形成一个分段对象 (Segment);Step5 (再写日志)
然后将索引数据写到日志 (Translog) 与 sharding-备份分片
;Step6 协调节点确保上述步骤都完成后,返回客户端写入成功的响应; -
(2)点击上架,首先从数据库中查询到上架商品的全部信息,然后将数据写入product索引,保存到ES中。
-
(3)ES保存成功后,在数据库更新商品的上架状态信息。
2、商品检索过程
-
(4)商品上架成功后。用户可以通过【点击商品分类】、【输入搜索关键字】、【选择筛选条件】三个部分进行检索。
-
(5)用户点击搜索后,将检索条件&&排序条件等信息放在url请求上,传到后台服务器进行检索;
-
(6)服务端根据url传递的参数构建DSL查询语句
BoolQueryBuilder类
,在ES中进行检索。在ES中的搜索行为是一个【查询后取回 (Query then Fetch)】
的过程;步骤 描述 Step1 首先,后台服务器向ES集群的某一个节点,发送查询请求(关键词key),该节点成为协调节点; Step2 协调节点将查询请求的key,广播到所有ES节点,这些节点的分片sharding就会各自处理查询请求; Step3 每个分片根据key进行数据查询,具体步骤如下:
① 首先,根据ES指定的分词器,将查询关键词key进行分词 (规范化、去重)
,分成多个词项term;
② 对于每一个词项term,分别根据内存中的term index (FST) 获取包含该term文档所在磁盘块block中的位置,同时输出倒排表Posting List;
③ 再对每个词项的倒排表进行合并或者取交集等操作,获得查询关键字最终的倒排表,进行打分排序;
Step4 【① Query阶段 (轻量级),每个分片通过查询倒排索引,返回的是文档ID集合,并不是数据】 待到全部分片的查询全部结束后,将所有分片的查询结果放入一个队列中,并将这些数据的文档ID、节点信息、分片信息全部返回给协调节点;
Step5 【② Scroll分析 (经过轻量级的Query阶段后,免去了繁重的全局排序过程)】, 由协调节点将所有的查询结果进行筛选 + 汇总 + 排序;
Step6 【③ Fetch阶段 (重量级),将筛选后的数据 (所有信息) 全部取回】, 协调节点处理完毕后,向包含筛选后文档ID的分片发送 get() 请求,待取回全部分片的数据后,协调节点将查询结果数据整合起来,返回给客户端;
3、检索商品的更新与删除
-
(7)当商品上架的信息发生变化时,需要对ES中的数据进行更新。但是由于ES底层的文档是通过【不可变的Segment段】存储数据的,因此不能向MySQL那样进行修改后再展示。为此,ES针对删除操作与更新操作是按照如下进行处理的。
操作 描述 更新 将旧的文档标记为deleted状态,然后创建一个新的文档; 删除 文档其实并没有真的被删除,而是在 .del 文件中被标记为 deleted 状态。该文档依然能匹配查询,但是会在结果中被过滤掉; -
(8)在ES的底层,当Segment段文件大小达到合并阈值时,会对Segment文件进行合并操作,在合并的过程中就会将这写标识了deleted状态的文档给物理删除掉;
二、说一下项目中登录功能是怎么实现的?
关键 | 描述 |
---|---|
登录 | 项目中的登录,是通过【OAuth2.0】+【redis存储Token】+【SpringSession】+【ThreadLocal】实现的 |
OAuth2.0 协议 | 现在很多网站的登录功能基本都提供了社交登录的操作,比如可以通过微信、支付宝等账号直接进行登录,而不需要注册再登录。我的商城项目中就是通过OAuth2.0协议实现了这种社交登录功能。以微信登录为例,具体的流程如下: (1)当用户点击微信登录按钮,浏览器会携带者client_id与回调地址访问微信的【认证服务器】。 (2)微信的【认证服务器】验证客户端的合法性之后,生成并返回给浏览器一个用户授权页面。 (3)用户同意授权后(登录微信),此时微信【认证服务器】就给【商城服务器】发放一个code码(此时商城就有资格访问微信的资源服务器了); (4)【商城服务器】拿着code码与回调地址,发送POST请求访问微信的【资源服务器】,换取access_token(访问令牌); (5)【商城服务器】拿到access_token后,就可以访问【资源服务器】,从而获取用户到的开放信息。至此,完成社交登录。 ![]() |
ThreadLocal 存储用户登录状态 | 用户登陆后,登录状态存储在ThreadLocal中,以实现多线程环境下不同用户登录信息的数据隔离。而且通过ThreadLocal,使得我们可以在同一线程中很方便的获取用户信息,而不需要频繁的传递session对象。 ThreadLocal面试题:那你说一下ThreadLocal是如何实现线程的数据隔离的? ①ThreadLocal类只是一个外壳,真正存储数据的是ThreadLocal内部的 ThreadLocalMap<ThreadLocal,value> 结构。ThreadLocalMap的引用是定义在Thread类中的。因此,实际上ThreadLocal本身并不存储数据,它只是作为key来让线程从ThreadLocalMap获取对应的Value。 ② 因此,每个线程创建ThreadLocal时,实际上数据是存储在自己线程Thread类内部的threadLocals变量中的,其它线程无法拿到,从而实现了数据隔离。 ![]() |
分布式会话 | 为了保证分布式场景下的登录状态一致性。用户登录之后,通过【SpringSession】 + 【redis】+【token】+【设置cookie跨域】机制来实现。 面试题:那你说一下是怎么实现分布式会话的? |
分布式会话实现的原理
难点 | 描述 |
---|---|
问题 | 在单机环境下时,由于HTTP协议是无状态的,导致服务器无法记录我们的登录状态,此时我们是可以用【cookies】+【session】解决的。 但是进行分布式扩展后,会发现我们在已经登陆后,分别访问不同的微服务时,系统依然提示我们去登录。 |
原因 | 针对这个问题,我查阅了相关资料。发现session的本质实际上时tomcat为我们创建的一个对象,它使用ConcurrentHashMap保存属性值。 但是tomcat本质也是一个Java程序,由于每个微服务监听的是不同的端口。那么从一个tomcat容器中(8084)创建的session,另外一个tomcat容器(8085)是肯定获取不到的,因此也就出现了分布式扩展后,提示我们登录的问题。 |
解决 | 针对分析的原因后,我们可以通过【设置cookie跨域分享】+ 【redis存储token】+ 【SpringSession】的方式解决。解决原理如下: (1)首先针对tomcat这个问题,我通过定义一个CookieSerializer方法,放大Cookies的作用域,以方便进行跨域。 (2)用户在某个微服务中登录后,将session中的用户登录信息以token的形式存储在redis中。 (3)然后开始引入SpringSession。具体的: ① 通过@EnableRedisHttpSession注解,导入RedisHttpSessionConfiguration配置; ② 该注解给SpringBoot容器中添加了一个组件SessionRepository,这个类是session的增删改查封装类; ③ 配置完成后,每个request请求都会经过一个SessionRepositoryFilter类,这个类是一个增强器,它的内部将原始的request、response进行了包装增强,形成了新的类( SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper)。 ④ 经过包装的request,在调用request.getSession()方法时,实际上调用的是SessionRepositoryRequestWrapper的getSession()方法,相当于从redis中获取session。从而实现了分布式session会话。 至此,解决了分布场景下提示登录的问题。 |