仿知乎的简单问答平台
本项目使用微服务技术。划分为4个微服务 1、zh-gateway :网关,所有请求的入口 2、zh-qa :核心,负责问题,回答,评论,话题模块 3、zh-user:负责用户模块 4、zh-search:全文检索,简单使用ES进行全文检索
###所用技术 springBoot,springCloud,springCloud alibaba ,redis,rabbitmq
nacos单机启动: startup.cmd -m standalone docker启动redis docker exec -it redis redis-cli 输入密码:auth
- nginx结合windows配置本地域名: nginx中必须配置:proxy_set_header Host $http_host; 设置为$http_host才不会改变原有请求的host;具体配置将nginx配置文件,监听端口为88的
####使用说明
- Fork 本仓库
- 新建 Feat_xxx 分支
- 提交代码
- 新建 Pull Request
1)问题的浏览量使用zSet类型存储到redis中。key为:visits. value为问题中各id及其当前时间段的浏览量
2)有人点击进入问题详情时就使问题的浏览量加1。即使redis事先没有他的的缓存也会存进去(存储或更新)
3)定时任务:每隔一段时间就先从redis中取出所有的问题id和浏览量并清空(按浏览量降序排名)。
4)然后取前5位作为热搜榜的数据:从数据库中查询前5个问题的详情和问题的所有回答详情。存入缓存。更新热榜数据(不删热点数据,让他自己过期)
5)热榜数据缓存:数据结构为list。以hot为key。将热点数据按排名一次写入redis。
6)读取问题和回答时,一律先去redis中查询。没有的话再查数据库,但是查询过后不用写入redis
7)问题缓存:String类型,key为problem:id。value是问题详细信息
8)回答的缓存:List数据类型: key为replies:id 。value为 该问题下的所有回答
1.问题:热点问题的回答可能会有很多人去回答。如果还是不更新回答原来的缓存。给人查看就会有数据不一致的问题。
2.方案:有两种解决方案
---1)不解决:因为热点数据每个一段时间就会更新。更新之后再进入问题详情就是去查询数据库了。就只有一段时间有不一致性的问题。最终是一致的。
---2)解决:用户在写回答问题的时候。判断用户是否在回答热点问题(问题缓存的是否存在)。如果是的话,写入数据库后。
直接更新redis中的key为replies中的问题。向list中插入一条数据。
虽然只有一段时间是不一致的。但是这段时间是这个问题频繁讨论的时候。用户希望尽快展示。所以采取方案2
上面方案是针对所有的回答的缓存不一致解决
有2个时间: 1.每隔一段时间刷新热榜 2.热点数据(问题详情,所有回答,所有精选回答)设置过期时间(不设置的话浪费内存) 方案:热点数据的过期时间应该大于刷新热榜时间的间隔。这里设置为刷新热榜时间的3倍时间 原因:热榜虽然更新,但是热点数据很有可能还是有很多人访问的,为了解决缓存雪崩问题,过期时间长一点。等他真的过时了 以上回答缓存的id都是问题的id。
--如果这段时间某问题浏览量不为0则将浏览量增加到数据库中。 --如果这段时间某问题的浏览量为0,就将其从redis中删除。认为这个问题不会再是热点,很少人访问。无需存储
1)回答的点赞使用String存储。key是:praise:id(id是回答的id);value是回答的点赞数量 2)当有人点赞时就令value++。 3)当点赞数量达到多少的倍数就将点赞数量更新到数据库中。
4)每个回答都维持一个set集合 。每个人点赞后就加入到一个set集合。 5)点赞前,判断该人是否给该回答点赞过了。 6)点赞后,将该用户的id加入该回答的set集合
7)查看回答详情,直接从redis中通过set集合的长度得到
注:这个方案是有冗余的,完全可以通过set集合的长度得到该回答的点赞数量。但多个set不好遍历,而且还要开启定时任务写入数据库
每个线程都有自己Map(ThreadLocalMap)用于维护自己的私有数据,这个数据不会被其他线程访问到,形成副本的隔离。 线程Thread使用ThreadLocal维护自己线程的独享变量,一个线程可以拥有多个ThreadLocal。 ThreadLocalMap 是ThreadLocal的静态内部类。他的key就是当前的ThreadLocal对象。
举例说明: 有2个线程执行的是同一个线程体,线程体内有1个ThreadLocal对象(是类变量)地址是@123。 线程1的ThreadLocalMap : {@123->"hello"}
线程2的ThreadLocalMap : {@123->"我不是很好"}
假设有2个ThreadLocal,地址分别为@123,@000;则: 线程1的ThreadLocalMap : {@123->"hello",@000->"我是线程1的000"}
线程2的ThreadLocalMap : {@123->"我不是很好",@000->"我是线程2的000"}
如上,每个线程都有自己的Map,各自访问自己的map,这就实现了副本的隔离
#####在本项目应用ThreadLocal保存用户的session。 改进前:拦截器拦截请求验证用户是否登录后,在业务场景需要用到用户信息的时候,还需要从session中获得用户信息。由于session是保存在redis中的,每次请求要访问2次redis,性能下降
改进后:在拦截器验证用户是否登录时,会拿到保存在session的用户信息。拿到之后,直接保存到ThreadLocal中,由于请求从拦截器到控制层,业务层等都是同一个线程的,所以可以很容易的在业务方法中拿到这个用户的session
问题:在微服务之间进行远程调用,请求会被另一个拦截器拦截,验证登录未通过导致请求失败 原因:远程调用时,feign会构造新的请求,由于没有将从前端发送过来的请求信息拷贝给新请求,会求生前端过来的请求信息 解决方案:将从前端过来的请求信息,特别是cookie信息拷贝一份给fegin构造的新请求。 1)通过 RequestContextHolder.getRequestAttributes()可以拿到当前线程的请求信息。前端的请求会分配一个线程进行处理,这个线程从控制层,业务层都是同一个。 在该线程中有独享变量保存请求的所有信息。这是利用ThreadLocal实现的。 2)远程调用之前,fegin会先构造请求,实现远程调用拦截器,让feign构造请求之前先执行这个拦截器,在apply方法中,将旧请求的信息头取出拷贝给新请求头
问题:异步远程调用,依然会丢失请求头 原因:异步远程调用时,会开启一个新的线程去执行远程调用,当前线程不是分配给前端请求的原线程了。所以原线程ThreadLocal中的数据访问不到 解决方案:异步调用feign之前,从原线程拿到请求头,再拷贝给异步线程
1.使用nacos做配置中心,像redis,mq这种各服务统一的实现复用。 2.mq消息的丢失问题和重复消费问题的解决 3.增加服务熔断机制,提高服务可用