仿抖音项目-Java重写版
服务名称 | 英文名 | 端口号 |
---|---|---|
网关服务 | service-gateway | 8080 |
用户服务 | service-user | 8021 |
视频服务 | service-feed | 8022 |
评论服务 | service-comment | 8023 |
点赞服务 | service-favorite | 8024 |
关注服务 | service-relation | 8025 |
聊天服务1(基于轮询) | service-message | 8026 |
聊天服务2(基于websocket) | service-chat | 8088 |
数据库 | mysql | 3306 |
缓存 | redis | 6379 |
注册中心 | nacos | 8848 |
消息队列 | rabbitmq | 5672,15672 |
由于是微服务架构,存在一个功能的实现需要多个服务间相互调用,为了能明确一个功能的服务调用流程,我们使用基于jaeger依赖的链路追踪
访问网址:http://114.132.160.52:16686/search
可以查询每个接口的服务调用流程
登陆后的绝大部分功能都是需要前端携带token请求,后端处理请求前先检验token的合法性。但由于每个接口都有这个token判断的逻辑,代码耦合度太高。
这里使用springboot的HandlerMethodArgumentResolver(参数解析器),通过在参数添加注解标识来拦截请求,鉴权后再执行后续逻辑。
要想获取视频的评论缓存,最简单的做法就是用该视频的id作为key,评论列表作为value存入redis中。每次有新的用户发布评论就将对应视频的评论缓存全部删除,生成新的评论列表存入进行更新。
如果这个视频非常火,用户评论数在短时间内剧增,也就出现了一些问题,:
- 缓存列表需要不断更新,缓存的命中率就十分的低,这种设计导致缓存意义不大。
- 频繁的序列化和反序列化:因为将对象从内存转化为二进制或json格式存入redis中需要消耗cpu资源进行序列化,频繁的更新增大了cpu的压力
- 随着评论数据量的增大,可能形成大key,不仅对redis服务有影响,反序列化出来的评论列表也就越大,有内存溢出的风险
首先我们肯定不能总是一次性把所有评论从数据库中都拿出来,可以先拿比如说300条,等到滑动到底了再触发请求再拿300条存入缓存。但是前端展示的时候是20条一批。
我们把这300条数据只取出主键索引,也就是评论id按评论时间升序存入redis的一个zset中【1-300】,让value内容可以减少。这个列表称为【评论id列表缓存】。如果拿出下一批就是【301-600】,key为: index-301_600
。
如果又来了一条新的,【601-???】,前端拿肯定是拿不到20条,那就再往前面的zset也就是【index-301-600】再拿20条,此时会拿出20条+的数据。同时新增一条K-V的具体评论内容
对于具体的评论内容,我们通过它的评论id和评论内容按K-V缓存来处理(string类型),拿到【评论id列表缓存】后遍历K获取每一条具体内容V。
每次需要更新缓存的时候我们就只需要更新【评论id列表缓存】,具体的string没必要变动
- 用户insert一条评论,清空该视频的【评论id列表缓存】,新发布的具体评论string单独缓存,旧的具体评论内容仍然保留
- 重新select出最新的300条评论,300个评论id存入新的【评论id列表缓存】,300条具体评论内容
- 返回前20条数据给前端
其中,第2步的【重新select出最新的300条评论】可以不需要,重新select的目的就是为了向用户展示最新的评论数据; 但如果前端能够在用户发布新评论后及时将这条新评论展示出来也能达到目的,此时用户看到自己发表的新评论其实并不是来源于后端,而是前端展示的假数据。但实际上这条新评论都已经存入数据库和缓存了;
- 如果拿一个【评论id列表缓存】的区间段数据不满足20条时会拿上一个区间段,为了减少下标的索引计算就索性再拿20条数据了。但这样就会导致每次拿到的数据条数不一定是20条,但至少始终是在【0-40】之间
- 随着数据量的增加,内存的占用肯定会不可避免的增高,这时可以将序列化的格式由json改为protobuf
获取一个视频的整体信息包括了:视频基本信息、评论数、点赞数、视频作者信息。起初是通过feign client同步调用获取,这样一旦某个接口调研时间较长就会导致整体获取响应时间延长
举个栗子:假设一个接口有以下三个任务:A、B、C,各任务的执行分别为2s、2s、3s。如果是同步机制,要想执行当前任务就必须要等前一个任务执行完成,这样总时间九尾就为2+2+3 = 7s。
如果是异步,在同一个时间段内,三个任务都是在执行的,这样总的执行时长就取决于最耗时的那个任务(任务C),也就是3s。
改进:创建线程池,使用调用者线程的拒绝策略,异步获取以上四个信息后,再统一返回整体视频信息的结果。
线程池的参数该设置成多少?
一般说来核心线程数的设置:
- 如果是CPU密集型应用,则线程池大小设置为N+1,线程的应用场景:主要是复杂算法
- 如果是IO密集型应用,则线程池大小设置为2N+1,线程的应用场景:主要是:数据库数据的交互,文件上传下载,网络数据传输等等
在这个获取视频信息的功能中,这些Feign任务其实都是数据库的查询任务,所以是IO密集型.
删除步骤:删除数据库的视频基础数据-------->删除OSS视频存储文件-------->删除视频相关的点赞、评论信息,其中数据库信息必须最先删除。
为什么要先删除数据库中的视频数据?
假设我们不先删除数据库的数据,而是删除点赞、评论的信息,如果删除完成后此时有用户获取该视频信息,就会发现视频能看,但是其点赞评论数据都没了,相当于进行了一个重置操作,这是不符合我们的需求的。只要我们最先删除了数据库的信息,接下来无论如何都不会看到这个视频了(即使OSS文件还存在着)。
其次,在这个删除视频过程中最耗时的其实是删除OSS视频文件。所以该请求的同步操作是很慢的,这里使用消息队列进行异步解耦。
点赞、评论信息我们需要删除,但并不依赖于视频模块。用户发起删除视频请求后,我们成功删除了数据库的数据就直接响应请求。其余三个操作通过MQ异步处理,至于能否一次就执行成功,就是MQ管了,以此提高用户体验。(当然用线程池也能达到一样的效果,其实就是嗯堆技术栈)
**使用:**启动后访问http://114.132.160.52:8088/douyin/page/login.html进行用户名设置即可使用
采用websocket的方式监听session中的用户,以及客户端发送的消息,对消息进行异步转发给各个客户端到前端展示消息,任何用户 每发一条消息都会存储到数据库,用户下次打开聊天框的时候会请求后端,后端查询数据库,将得到的数据根据时间顺序返回消息列表
前端遍历展示消息
前端机制:
- 定期发送http请求,查询消息,其中请求携带上这一次的请求时间
- 用户打开聊天界面,如果是第一次请求,请求时间是0。之后就是实时的时间戳
后端机制:
- Mapper每次根据请求时间进行过滤,查询出小于这个时间戳的消息记录列表
- 判断前端传来时间戳,如果为0,说明是第一次,直接返回消息记录列表,并且redis记录列表大小
- 后续的轮询中,每次mapper查出来的列表大小都要与redis中缓存的大小相比较,如果前者大,说明用户发送了消息,此时将新消息返回,更新缓存的列表大小
- 如果两者大小一样,说明用户没有发新消息,只是重新进入聊天界面,这时返回空集合
接入chatgpt:
- 将chatgpt设置为一个用户,默认每个用户与其互关
- 发送消息判断toUserId是否为gpt的id即可
- 异步向gpt问答请求,gpt的回复存入数据库
- 使用redis实现了一问一答,要求gpt未回复之前不能发问题
本模块涉及到:用户关注/取消关注功能的实现,获取用户的关注用户列表和粉丝列表以及获取朋友列表。
整个模块中,我们维护了两个key:
followUserIdKey-----当前用户的关注者id列表,followerIdKey----当前用户的粉丝id列表。
这两个key可以帮助我们快速获取到关注用户列表和粉丝列表,而不用再去数据库中进行查询操作,当然也能帮助我们获取朋友列表。
1、在用户/取消关注功能的实现中,我们需要先从数据库中查询是否有相关用户的记录,有则说明两个用户之间存在过关注/取消关注的操作,此时我们根据actiontype判断是关注操作还是取消关注操作。
若没有相关记录,则是关注操作,插入相关记录即可。
在这上面的过程中,上述这两个key会有相应的增删操作,我们需要通过执行lua脚本,来保证redis命令的原子性—— 当用户关注另一个用户时,对另一个用户来说,他多了一个粉丝。当判断到lua脚本执行失败时,我们需要回滚数据库和redis,保证redis缓存和数据库的数据一致性,操作无误后删除该用户缓存以及视频缓存。
2、对于获取关注用户列表和粉丝列表,我们先尝试从缓存中获取,获取不到再去数据库中获取,通过获取保存在redis中的相应的id列表,调用user服务,返回相应的用户列表,然后再将用户列表缓存进redis中。
3、获取朋友列表,只需比对用户的关注用户id和粉丝id有没有交集,交集部分即为互相关注的用户,调用user服务即可获取朋友列表(message模块还未完善)。
4、缺点:使用redis保存id列表可以减少获取关注用户和粉丝用户操作时都从数据库中查询记录的操作,这在一定程度上可以减轻mysql的负担,但是维护的两类key在关注/取消关注操作时要特别注意原子性问题,这会让关注功能变得繁琐,而且可能会出现数据不一致的问题。