项目相关
实习介绍:
实习主要是学习了公司的相关业务,开发了一个针对机器审核模块的一个自动化回归测试工具,另外简单学习了公司的微服务。因为公司用微服务用的比较多,通过consul使用一台线上机器作为跳板,连接到美东的服务。
确实学到了很多东西,但是大部分都是接触到了,但是如果要真正弄清楚怎么实现的,还需要自己额外花时间研究,我在研究机审自动化这个模块的服务架构的时候,发现有一个服务会把数据存入到ES中,当时不太明白,为什么要在ES中也存一份数据,后来学习到ES了,了解到这个搜索引擎也是一个特殊的数据库,要实现搜索,必须把待搜索的数据存到ES中,ES并不是从本地数据库中搜索数据,而是从他自己的数据库中搜索数据。
比如公司有一个日志查询系统,可以根据关键字或者是日志id查询到产生的日志,这个系统有一个1小时左右的搜索延迟,就是说当前产生的数据必须等到一小时后才能搜索到,我当时就比较好奇,为什么这个系统要这样设计,为什么不实时更新,后来等我学习了Elasticsearch后才明白,这个搜索系统应该是采用ES来是实现的,因为ES的倒排索引使得关键字搜索在全文检索中的效率非常高,而且这个ES有一个延迟写策略,因为ES内部采用的是分段的形式来存储数据的,新增的数据并不会马上写到数据库中,而是会在内存中临时间建立一个段,这个段用来缓存数据,等到一定的时间间隔,生成提交点刷新到磁盘。这时就可以搜索到数据了。之所以要这样做是因为每次增加段后都需要重新建立索引,这个开销比较大,应该尽量减少这个开销。
项目经历:
牛客高薪求职项目课(好项目,强推,就是太贵了):springboot + redis + kafka + elasticsearch
BBS论坛:基于SSM框架
会议报名通用系统:jsp + servlet
以下所有内容均是基于《牛客高薪项目求职课》
项目的亮点
登录功能性能很高
热榜加入缓存中
向点赞、关注、统计UV, DAU数据都是在Redis中
有一个搜索功能
引入了kafka来接收异步请求
使用了spring security来进行权限控制
统一事务处理,统一日志处理,
哪里用到了缓存,Redis
点赞,使用“like”关键字加上实体id和实体类型作为键,点赞用户id列表做为值,记录每个实体,比如帖子,评论被点的赞,实体被点赞小给实体的作者加一个赞,所以每个用户被点赞的次数。这两个操作也是在同一个事务中
关注,使用”followee" 加上实体id和实体类型作为键,用户列表作为值,组成了某用户的关注列表。存在缓存中方便修改,如果把数据存在数据库中,字段太少,而且这些操作一般比较频繁,会降低系统性能。因为这些数据一般比较小,所以存在缓存比较合适。
热榜,把热帖存在Caffine中,避免大量请求打到数据库,防止数据库被打死。也降低数据访问延迟。
重新计算分数,也不是把所有帖子id都放到redis中,这样数据量太大了,而且大部分帖子都分数都不会发生变化,所以我们在redis中通过一个set,保存所有分数发生了变化的帖子id, 比如点赞会使分数发生变化,评论也会是分数发生变化,包括加精等,在这些操作中加入把帖子id加入redis中指定的set中,定时清空redis中的set, 把set中的帖子的分数重新计算一遍。计算往后需要清空Caffine中的缓存,使得热榜能获取到最新的帖子。
登录凭证LoginTocket对象,把用户的token为键和用户的LoginTicket为值保存在redis中,每次请求,浏览器都在cookie中带上这个token, 然后去redis找,判断是否存在对应的LoginTicket对象,并且判断是否过期,如果存在且没有过期才去数据库中查找用户,把用户对象放到ThreadLocal中,方便业务使用。
用户对象User, 把常用对象存储在缓存中,减少对数据库的请求,降低数据库的压力。但是如果用户对象的数据发生改变,比如头像发生改变,密码发生改变,为了保证数据库和缓存中对象的一致性,必须使用aside旁路缓存更新策略,把更新数据库中的对象数据,然后删除缓存中的对象数据。
UV和DAU:
UV: 独立用户,需要通过用户IP排重统计数据,每次访问都要进行统计,HyperLoglog,性能好,且存储空间小
DAU: 日活跃用户,通过用户ID排重统计数据, 访问过一次,则认为其为活跃, BitMap性能好,且可统计确切的结果
登录的过程(设计得比较好的功能,也可以说是项目的难点)
在controller中校验验证码,如果不正确,直接返回验证码错误 2.如果正确,在传到service中处理,判断账号密码格式是否正确,如果正确去数据库中查找用户表,找到之后判断是否激活,以及输入的密码加上盐取md5之后是否等于查到的密码,如果等于才会往下继续操作 3.生成登录凭证,设置一个uuid作为token, 过期时间,用户id后以的token为键和用户的LoginTicket为值保存在redis中,4. 在controller判断传回的model数据中是否存在token, 如果存在token, 说明登录成功了,此时把token放到cookie中传给浏览器,至此就登录成功了。 4. 用户下次发起请求的时候,就会在cookie中带上这个token, 服务器就会根据这个token在redis中寻找,判断是否存在对应的LoginTicket对象,并且判断是否过期,如果存在且没有过期则根据loginTicket对象中的用户id去redis缓存中寸照对象,如果找到了则把用户对象放到ThreadLocal中,方便后续业务使用。如果没找到去就数据库中寻找,然后把对象放到缓存中,也放到ThreadLocal中。
这个功能中主要想讲的就是通过LoginTicket作为登录状态,ThreadLocal中的对象作为用户状态来代替session, 因为session在分布式架构中不太好管理,
通过缓存来缓存loginticket对象和用户对象,降低访问延迟和降低数据库的访问压力。
项目难点:
性能的提升,以及方便未来分布式架构调整维护做出的预先准备
刚开始服务还没上服务器的时候,因为要做压测,所以构造了30w条帖子数据,但是发现点到首页后点击下一页要过很久才会响应,原因就是数据库数据太多了,检索慢,所以后来给这个查询加上了索引,加上索引之后发现速度有很大提升,点击下一页几乎和1w条数据的速度一样快。
做压测的话,对热帖进行压测,使用100条线程,在1秒钟只能循环访问这个热帖页面,在不加缓存情况下,吞吐量只有每秒10次左右,加上Caffine这个一级缓存后吞吐量就达到了180+次每秒。性能提升了18倍多。服务上线后没有测试过。
考虑到未来如果项目变成分布式的项目,session的共享问题,所以没有用session来保存登录状态和用户状态, 而是把当前对象的状态存在一个ThreadLocal里面,每次都从当前线程中取出threadlocal中的用户状态来使用。其次登录状态本来是存在数据库中,每次登录都会给这个用户生成一个随机的uuid, 把这个uuid回传给浏览器的cookie, 在一个LoginTicket表中新增一条记录,记录这个用户的登录时间以及过期时间,用户访问功能页面的时候浏览器就会主动带上这个uuid, 后台在过滤器中从cookie中拿到这个uuid去LoginTicket中找,通过判断能否找到记录来判断用户是否登录,如果登录了根据loginTicket中的用户id去用户表中查找记录放到ThreadLocal中。
这样虽然避免了session的使用,但是确实多了一次查loginTicket表的开销,所以后来把loginTicket对象放在redis中,就减少了一次查表操作,提高了性能。
为了避免查询效率低,对于频繁被用来做查询条件的字段建立索引
多处使用redis, 热榜等,降低数据库的压力。
排错的话主要是在服务更改了几个bug后重新上线的时候, 因为忘了密码,所以需要安装kakfa
事务案例
添加了评论之后必须修改帖子中评论数量这个字段,这两个操作必须在同一个事务中进行,要么都成功,要么都不成功。或者说电商系统中的订单支付和修改消费券使用状态两个操作,也是要么都发生,要么都不发生。
redis事务
点击关注,需要在当前用户的关注列表中增加一个对方用户id,也需要在对方的粉丝列表中增加一个粉丝id。取消关注也是一样,都要在一个事务中完成。
冗余字段案例
帖子表中冗余存储了一个评论数量字段,因为帖子列表中会展示每个帖子的回帖数量和点赞个数,所以为了减少对评论表的查询次数,所以设置了这个冗余字段,虽然增加了一些内存开销,但是换取了较大的性能提升。
消息表中存储了一个会话id字段,在私信功能的会话列表中会展示全网所有用户和当前用户的私信会话,每个会话中记录了每个网友的所有私信内容哦,虽然这个会话可以通过私信发起人和接收人唯一确定,但是为了提升查询效率,也为了简化设计,增加了这个会话id字段,这个字段由私信发送方和接收方的用户id通过下划线拼接而成,小的id在前。
项目改进方向:
为了抗住更大的请求应该改成分布式的架构,主从分离。加二级缓存,