version:1.0
文章目录
- 登录部分
-
- 是如何记录用户登录状态的? / 登录的业务逻辑是怎么实现的?
- cookie是什么?和session有什么区别?
- Interceptor拦截器的使用? / 为什么使用?
- Redis部分
-
- 为什么用Redis作为MySQL的缓存?
- 项目中哪里用到了Redis?
- 为什么用Redis保存点赞/关注的数据?使用了哪些数据类型?用到了哪些命令?
- 为什么用Redis保存验证码、登录凭证、用户信息?
- 为什么用Redis保存UV/DAU?使用了哪些数据类型?用到了哪些命令?
- 用过Redis的事务吗?怎么使用的?
- Redis 有哪些数据类型?SDS 了解么?
- Kafka部分
-
- Kafka是什么?
- 为什么使用Kafka?
- Kafka消息模型?
- Kafka如何保证数据不丢失?
- Kafka如何保证消息不重复消费?
- Kafka如何保证消息的顺序性?
- 项目其他
-
- 为什么做这个项目? / 项目的收获?
- 项目的架构是怎样的? / 模块? / 介绍下项目?
- 统一异常处理怎么做的?
- 统一记录日志怎么做的?
- 项目难点?/ 遇到的问题? / 项目最有挑战的模块,如何解决 ?
- 项目的亮点?
- 你是如何解决项目中遇到的问题的? / 解决问题的思路?
- 项目中使用的设计模式?
- 项目还有什么存在的问题和完善解决方案 ?
登录部分
是如何记录用户登录状态的? / 登录的业务逻辑是怎么实现的?
使用Cookie来解决用户无状态的问题。在用户登录时,我们给用户设置一个登录凭证ticket【ticket中包含userid,当前状态的信息】,放入cookie返回给浏览器。response.addCookie()
在用户下一次再访问时,浏览器会携带cookie(ticket),服务端可以根据cookie确定用户的登录状态。
确定用户登录以后,可以通过ticket查询到用户id,再通过用户id来查询用户的具体信息,之后把user放⼊model,再由模板引擎渲染后,返回网页给客户端浏览器。
cookie是什么?和session有什么区别?
cookie是保存在客户端的,不占用服务器资源。对于并发量比较高的网站,比如我们做的交流平台,就使用cookie。
session是保存在服务端的,每个用户都会产生一个session,那么如果并发访问多,每个用户都会生成session,就会很占据资源,所以对于并发访问不多的网站,比如之前做的后台管理系统,可以使用session,也更加安全。
Interceptor拦截器的使用? / 为什么使用?
- 拦截用户凭证[
loginTicketInterceptor
]:拦截器做的就是将ticket拦截,之后根据ticket查找user,最终返回到页面上。
- 拦截未读消息[
messageInterceptor
]:在拦截器中查询未读消息数量,保存到ModelAndView中返回给页面。
- 拦截网站数据[
dataInterceptor
]:在拦截器中记录网站UV和DAU。UV根据ip统计,DAU根据用户统计。
拦截器是不破坏业务逻辑的,可以定义一些通用的行为。因为我们所有的请求中都需要显示用户的登录信息,所以使用拦截器实现这个功能。
Redis部分
为什么用Redis作为MySQL的缓存?
追问:为什么不用Memcached?
两者都是基于内存的数据库,一般都用来当做缓存使用,并且性能也比较高。使用Redis而不使用Memcached的主要原因如下:
- 但是Redis的数据结构更加丰富,而Memcached只支持K-V的数据类型;
- 并且Redis 支持数据的持久化,可以将内存中的数据写入AOF或者保存在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
- Redis 还可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务功能,实现命令的顺序执行。
总之,Memcached是解决简单缓存问题的可靠选择,但是Redis的功能更多,可以解决更多复杂的使用场景,所以选择Redis作为缓存。
项目中哪里用到了Redis?
- 使用Redis作为缓存数据库来保存数据,比如点赞的数据、关注的数据。
- 验证码、登录凭证、用户信息都缓存在Redis中。
- 统计网站DAU、UV的数据也保存到了Redis中。
为什么用Redis保存点赞/关注的数据?使用了哪些数据类型?用到了哪些命令?
因为点赞、关注是一个很高频的操作,如果直接在数据库中使用Count,效率很低,所以可以使用Redis来提升性能,从缓存中查询数据,会比直接在硬盘中查询数据要快。
使用的数据类型:
- 给帖子、评论点赞使用Set来存储。key代表某个帖子或者评论,value表示点赞的用户Id。
- 使用Set进行存储,可以防止同一个用户多次点赞的情况,因为Set可以去重。
- 用户收到的赞使用String来存储。key代表具体用户,value就是收到的赞数。
- 比如用户的帖子获得一条点赞,那么就在给帖子增加点赞数的同时,给用户收到的赞 + 1 即可,所以就使用String来进行存储。
- 关注、粉丝数据使用Zset来存储,key代表用户的关注 / 粉丝 ,value代表关注 / 粉丝 userId。关联的score 来保存关注时间,这样我们可以按照关注时间进行排序。
- 使用Zset可以防止重复关注的问题,并且我们可以根据关注事件进行排序。
使用的命令:
- Set:add、remove、ismember、size
- String:decrement、increment
- Zset:add、remove、ZCard【统计元素个数】、ZREVRANGE【时间从大到小排序】,ZRANGE【从小到大排序】
为什么用Redis保存验证码、登录凭证、用户信息?
用Redis保存验证码:
- 验证码需要频繁的刷新和访问,对性能的要求比较高
- 并且验证码不需要永久保存,需要设计过期时间来节约空间。
- 之前验证码都是保存在session中,在分布式部署中session会出现共享问题。将验证码保存到Redis中避免分布式session不可用的问题。
用Redis保存登录凭证、用户信息:
- 之前将登录凭证保存到MySQL中,导致每次都需要去login_ticket表中查询登录凭证,由于每次访问页面都需要在拦截器中对登录凭证进行检查,访问频率很高,所以用Redis进行存储。Redis是内存型数据库,访问很快。
- 需要在拦截器中根据登录凭证去查询用户信息,访问频率很高,所以用Redis进行存储。
为什么用Redis保存UV/DAU?使用了哪些数据类型?用到了哪些命令?
用户每访问一次网页,就需要向数据库中插入一条数据,如果日活很高,会消耗数据库资源。
并且如果要进行月度、年度的统计会使用count、group by这样效率很低,所以使用Redis来保存UV / DAU。
- UV:独立访客,通过IP进行排重计算。HyperLogLog就可以满足去重要求。并且每个 HyperLogLog 键只需要花费 12 KB 内存,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
- ADD方法:传入VALUE = IP进行统计。
- UNION方法:合并HyperLogLog的集合到新的HyperLogLog。
- DAU:日活跃用户,通过用户ID进行去重。使用Bitmap一连串的二进制数组,可以统计精确结果,并且占用空间很小。
- SETBIT方法:通过一个偏移值 offset 对 bit 数组的 offset 位置的 bit 位进行读写操作。
SETBIT login_status 101 1
表示ID = 101 的用户 已登录。
- OR运算:统计一周、月度的活跃用户。
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。
用过Redis的事务吗?怎么使用的?
Redis的事务提供了三个操作:
- MULTI:事务开始执行的命令。
- EXEC:执行队列中的命令。
- 在MULTI和EXEC之间发送的所有命令都会被记录下来,并在执行EXEC命令时按照顺序执行。这些命令并不会立即执行,而是被放入一个队列中等待执行。
- DISCARD:取消事务。
- WATCH:监视key,执行事务之前key如果被修改,事务的执行会被打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。项目中点赞操作就使用了事务。当用户执行点赞操作,我们需要在Set中增加一条数据,并且给被点赞的用户获赞数 + 1。这是两个操作,防止别的命令插队,所以使用事务。
注意:Redis不提供事务的回滚。所以这里实际的作用不大。
Redis 有哪些数据类型?SDS 了解么?
String、List、Hash、Set、Zet都是比较常用的数据类型。其中 String 数据类型的底层数据结构是 SDS(simple dynamic string,SDS)。
SDS这个数据结构中存在几个字段:
- len,记录了字符串的实际长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
- alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过
alloc - len
计算出剩余的空间大小,并且根据需求自动修改空间大小,也不会出现前面所说的缓冲区溢出的问题。
- buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。不会出现二进制安全的问题。
- flags,用来表示不同类型的 SDS。
对比C语言,主要有以下优势:
- O(1)复杂度获取字符串长度
- C 语言的字符串长度获取 strlen 函数,需要通过遍历的方式来统计字符串长度,时间复杂度是 O(N)。
- 而 Redis 的 SDS 结构因为加入了 len 成员变量,那么获取字符串长度的时候,直接返回这个成员变量的值就行,所以复杂度只有 O(1)。
- 二进制安全
- 因此, SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。
- 不会发生缓冲区溢出
- Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过
alloc - len
计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。
- 而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小,以满足修改所需的大小。
- 节省内存空间
- SDS 设计了不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。
- 使用了专门的编译优化来节省内存空间,即在 struct 声明了
__attribute__ ((packed))
,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。
Kafka部分
Kafka是什么?
是一个分布式流式处理平台,流平台一个关键的功能就是消息队列。
项目中使用Kafka消息队列,对评论、点赞、关注功能发布通知,封装为Event实体类。
消费者负责将消息队列中的Event取出,并将其封装为Message对象,并持久化到数据库中保存。
为什么使用Kafka?
- 深入了解:
Kakfa的性能好,采用的是磁盘的顺序I/O,并且生态比较完整,遇到问题可供查阅的资料也会比较多。
- 横向对比:
当时了解到除了Kafka还有其他的消息队列,比如RabbitMQ。但是相比之下还是选择了Kafka,因为我们要做的是一个系统通知,在处理大量数据时,kafka的性能更高,但是RabbitMQ相比Kafka的消息可靠性更高。
RabbitMQ的灵活性要更好,允许自定义消息头,然后通过一些特殊的 Exchange,很简单的就实现了消息匹配分发。开发几乎不用成本。
Kafka消息模型?
- Producer将生产者发布的消息放到Topic中,Topic作为消息的载体。
- 这个Topic实际是在一个Broker中,一个Broker(代理)有多个Topic。
- Partition(分区)是Topic的一部分,一个Topic可以有多个Partition,Partition作用就是消息队列中的队列。消息在被追加到 Partition的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。
- 分区采用多副本机制。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。
- Comsumer消费者被动消费Topic中的数据。
Kafka如何保证数据不丢失?
- 生产者中:生产者发送给Broker消息可能丢失。
- 可以使用ListenableFuture类,来添加回调函数,打印生产者发送失败的原因。
- 给 retries (重试次数)设置一个比较合理的值,一般是 3。
- Broker中:Broker中有可能出现存储丢失的问题。
- 采用acks = all机制,会在所有分区副本follower保存之后,leader发送一条成功响应。
- 消费者中:offset已经提交,但是customer宕机导致消息丢失。
- 采用手动提交offset。手动提交可以保证消息至少被消费一次。在消费者消费完后设置异步提交,在finally中设置同步提交。【finally中的同步提交是最后的一个把关,防止异步提交出现异常】
Kafka如何保证消息不重复消费?
消息重复消费的原因就是有可能customer已经消费了数据,但是offset没有成功提交,这种情况的话我们采用手动提交offset。手动提交可以保证消息至少被消费一次。在消费者消费完后设置异步提交,在finally中设置同步提交。【finally中的同步提交是最后的一个把关,防止异步提交出现异常】
我在项目中实际是这样处理的,就是在处理消息的时候,因为我们将event封装为了Message对象,我们的Message有一个唯一标识id,那么我们在消费数据时就会判断这个id是否存在这个Message表中,如果存在就不进行消费。我觉得这种方法是最有效的。
Kafka如何保证消息的顺序性?
由于消费者组中的某一个customer可以会维护多个分区,但是只能保证一个分区中的消费顺序性,对于跨分区的消息顺序性无法保证。所以,如果想要保证按顺序处理,就要只提供一个分区。或者相同的业务只在同一个分区下存储和消费。
实际操作:在执行send时,可以指定分区号来进行发送; 或者设置同一个key来决定存储到同一个分区。
实际在这个项目中不需要保证消息的顺序性,因为都是通知类型,没有顺序可言。所以我这里只使用了两个参数的send方法,发送topic和event。
项目其他
为什么做这个项目? / 项目的收获?
做这个项目是因为当时自己在学习过程中想要巩固所学知识,也想要把自己学的东西运用到实际当中,就去网上找了一个项目,看评价也比较好,就自己实现了一遍,整体项目和自己之前学的Mybatis、Spring、MySQL、Redis相关的知识都在里面用上了,让自己从实践应用的角度更好的理解了这些技术,也提高了自己debug的能力还有解决问题的能力,自己培养了解决问题的逻辑
项目的架构是怎样的? / 模块? / 介绍下项目?
底层基于Spring Boot。项目可以分为几个模块:
- 权限模块【负责用户注册、登录、权限控制】,主要用到的技术是Spring Security、Interceptor拦截器。
- 核心模块【首页、帖子、评论、私信、统一异常处理、统一记录日志】,异常处理和记录日志主要运用了Spring AOP的思想,针对项目中的代码进行一个横向的扩展。
- 性能模块【点赞、关注、用户缓存、统计数据】,主要使用Redis保存点赞和关注、统计UV、DAU数据。并且缓存用户的信息、验证码、登录凭证到Redis,减小数据库访问压力。
- 通知模块【系统通知,点赞、评论、关注】,主要使用Kafka消息队列来发送通知,并且将通知转换为Message存到表中,使用id判重来解决kafka重复发送数据的问题。
项目介绍:项目基于Spring Boot,大体可以分为四个模块,权限、核心、性能、通知。
权限模块比如用户的登录注册,权限控制;核心模块比如首页,帖子,统一异常处理,统一记录日志;性能模块基于Redis包括点赞,关注,统计数据;通知模块基于Kafka,使用Kafka来发送通知。
统一异常处理怎么做的?
使用注解的方式统一异常处理,具体会使用到 @ControllerAdvice
+ @ExceptionHandler
这两个注解
这种异常处理方式下,会给所有或者指定的 Controller
织入异常处理的逻辑(AOP的运用),当 Controller
中的方法抛出异常的时候,由被@ExceptionHandler
注解修饰的方法进行处理。
统一记录日志怎么做的?
使用Spring AOP的思想,把所有的Service方法标记为切入点PointCut,在调用目标方法之前,记录日志。使用的是AspectJ框架,相比于AOP是静态代理,性能要比AOP好一些。
项目难点?/ 遇到的问题? / 项目最有挑战的模块,如何解决 ?
- 难点一【技术问题】
使用Spring Security时,点赞后会报错说找不到csrf字段。去网上找到解决方案就是在Spring Security的配置类中关闭跨域请求伪造的功能。
但是一直都很奇怪为什么只有点赞这个功能不行。后来去StackOverFlow用关键字搜索了一下问题,Spring Security 默认开启csrf,可以防止跨域攻击。但是只会在POST表单中自动添加csrf_字段【token】防止跨域请求伪造。
但是点赞功能是异步发送的POST请求。在异步的ajax请求中,Spring Security不会自动添加csrf字段,所以就会报错找不到。
- 技术难点二
项目中还使用了Elasticsearch和Redis,他们底层都使⽤Netty,导致这两者启动的时候,会有冲突,主要是ES底层的代码尝试启动Netty时,发现Netty已经启动,然后报异常。这里网上有很多解决方案,就是我们可以在项目启动时,就设置ES不要检测Netty是否初始化就可以。
- 难点二【业务问题,也不算难点,算是一个经验】
当时写完了用户发布帖子的功能,当时新注册了一个用户,之后用这个用户发布了帖子,后面我在数据库中把这个用户的信息删除了,再访问首页就报错了。当时没有意识到是因为删除用户信息导致的报错,就自己打了一个断点走了一遍,最后发现是在根据userId获取user时报错,userId为空。当时第一反应是加一个if条件判空,但是后来一想不需要这么做,因为是人为修改了数据库,实际不会出现这种情况。后来就把数据库中discuss_post那条数据给删掉了。
- 给我的经验教训:报错要注意查看报错信息,自己打断点找一下错误的原因。
项目的亮点?
其实就是写到简历上的哪些:
- 使用Redis + Caffine 构建二级缓存,QPS得到了10倍的提升。
- 使用Kafka,基于生产者和消费者模式,发送系统通知。并且Kafka还可以保持一个很好的性能,相比RabbitMQ,适合发送大量的数据。
你是如何解决项目中遇到的问题的? / 解决问题的思路?
- 非业务功能:看一些博客,尝试一些博客中的解决方案;去StackOverFlow这样的平台,搜索解决方案;到这步就能解决大部分的问题;最后可能会去看一下官方文档中有没有对应的描述,去弄懂一下原理。
- 举例:当时学校让基于SQL Server实现增删改查的项目,因为自己没用过SQL Server,用的Java17和 SQL Server 2022的版本,启动时报错无法通过SSL建立安全连接,当时先去国内一些博客搜索解决方案,但是大部分都是千篇一律,并且文章质量不高,也没有实质性的原因,并且相关的文章很少。后面就使用StackOverFlow通过关键字来查询解决问题,推荐是在连接字符串中设置一个encrypt=true;trustServerCertificate=true,这个的意思就是JDBC 将不验证SQL Server的 TLS 证书,但是这个设置常用于允许在测试环境中建立连接,实际开发中可能会出现安全问题。后面就去微软的官方文档中研究了一下,官网中推荐使用keytool导入SQL Server的证书,后面就将这个证书导入进JDK中,就没有出现问题了,我觉得通过这个也提升了自己解决问题的一个能力。
- 业务功能:首先要看报错原因,看看原因中是在哪里报的错【定位问题】,之后自己打断点走一遍【分析原因】,就能解决基本的问题【解决问题】。
项目中使用的设计模式?
项目还有什么存在的问题和完善解决方案 ?
一些实现的细节要注意吧。比如判空之类的。完善的话,如果数据量增多,数据库方面可以考虑分库分表。
完。