项目相关功能介绍:

介绍一下项目:

这个项目的整体结构来源于牛客网,主要使用了Springboot、Mybatis、MySQL、Redis、Kafka、等工具。主要实现了用户的注册、登录、发帖、点赞、系统通知、按热度排序、搜索等功能。另外引入了redis数据库来提升网站的整体性能,实现了用户凭证的存取、点赞关注的功能。基于 Kafka 实现了系统通知:当用户获得点赞、评论后得到通知。利用定时任务定期计算帖子的分数,并在页面上展现热帖排行榜。

一、 发送邮件功能:

01 你如何在 Spring Boot 项目中实现发送邮件的功能?

“在 Spring Boot 项目中,我通过 Spring Email 技术实现了发送邮件的功能。首先,我导入了 spring-boot-starter-mail 依赖,然后在配置文件中设置了新浪邮箱的 SMTP 服务参数,包括主机名、端口、用户名、密码和协议,并启用了 SSL 安全连接。接着,我创建了一个 MailClient 工具类,封装了发送邮件的逻辑,使用 JavaMailSender 组件来发送邮件。对于 HTML 格式的邮件,我使用了 Thymeleaf 模板引擎,通过模板生成动态内容并发送。最后,我编写了测试类,验证了邮件发送功能是否正常工作。”

02 如何配置邮箱服务?

选择了新浪邮箱作为邮件服务商,并在邮箱设置中开启了 SMTP 服务。在 Spring Boot 项目中,我通过配置文件设置了邮箱的 SMTP 参数,包括主机名(如 smtp.sina.com)、端口(如 465)、用户名、密码和协议(比如spring.mail.protocol设置参数为smtps,spring.mail.properties.mail.smtp.ssl.enable=true)。启用 SSL 是为了确保邮件发送的安全性,防止信息泄露。

03 你如何发送 HTML 格式的邮件?

为了发送 HTML 格式的邮件,我使用了 Thymeleaf 模板引擎。首先,我创建了一个 HTML 模板文件,放置在 templates/mail 目录下。然后,我在代码中使用 TemplateEngine 处理模板,传入动态变量并生成最终的 HTML 内容。最后,我调用 MailClientsend 方法,将生成的 HTML 内容作为邮件正文发送。

二、注册功能

01 注册功能的开发流程吗?

  1. 访问注册页面:用户访问注册页面,前端通过简单的请求加载页面。我使用 LoginController 处理这个请求,返回注册页面的模板路径。
  2. 提交注册数据:用户填写表单并提交,后端接收数据并进行验证。我在 LoginController 中定义了一个方法,处理提交的注册数据。
  3. 数据验证:检查用户名、密码、邮箱是否为空,以及是否已存在。我使用了 commons-lang 工具包进行空值判断,并通过数据库查询验证用户名和邮箱的唯一性。
  4. 生成激活信息:生成随机字符串作为激活码,并对密码进行 MD5 加密。我创建了一个 Utils 工具类,提供了生成随机字符串和 MD5 加密的方法。
  5. 保存用户数据:将用户信息插入数据库,设置初始状态(如未激活)。我使用 MyBatis 将用户数据插入到 user 表中。
  6. 发送激活邮件:使用 Thymeleaf 模板引擎生成 HTML 邮件,通过 Spring Email 技术发送激活链接。我创建了一个 MailClient 工具类,封装了邮件发送的逻辑。
  7. 激活账号:用户点击激活链接后,后端验证激活码是否匹配。如果匹配,更新用户状态为已激活;如果不匹配,返回错误信息。我在 LoginController 中定义了一个方法,处理激活请求。

02 数据验证与错误处理

在注册功能中,我进行了以下数据验证和错误处理:

  1. 前端验证:使用 HTML5 的 required 属性和 JavaScript 进行初步验证,确保必填项不为空。
  2. 后端验证:在业务层检查用户名、密码、邮箱是否为空,以及是否已存在。我使用了 commons-lang 工具包进行空值判断,并通过数据库查询验证用户名和邮箱的唯一性。
  3. 错误处理:如果验证失败,返回错误信息,前端显示错误提示。例如,用户名已存在时,提示用户更换用户名。
  4. 默认值保留:在错误页面中,保留用户已输入的值,提升用户体验。我通过 Thymeleaf 的 th:value 属性,将用户输入的值回显到表单中。

03 邮件发送与激活功能如何实现的?

  1. 生成激活信息:在用户注册时,生成随机字符串作为激活码,并对密码进行 MD5 加密。我创建了一个 Utils 工具类,提供了生成随机字符串和 MD5 加密的方法。
  2. 发送邮件:使用 Thymeleaf 模板引擎生成 HTML 邮件,包含激活链接。通过 Spring Email 技术发送邮件。我创建了一个 MailClient 工具类,封装了邮件发送的逻辑。
  3. 激活账号:用户点击激活链接后,后端验证激活码是否匹配。如果匹配,更新用户状态为已激活;如果不匹配,返回错误信息。我在 LoginController 中定义了一个方法,处理激活请求。
  4. 结果处理:根据激活结果,跳转到登录页面或首页(比如激活成功返回登录页面,要是重复激活或者激活失败返回到首页),并显示相应的提示信息。

04 如何使用模板引擎实现的代码复用?

我使用 Thymeleaf 模板引擎实现页面复用:

  1. 代码复用:将公共部分(如页头、页脚)提取为单独的模板文件,通过 th:replace 属性在多个页面中复用。例如,我在 header.html 中定义了页头代码,并在注册页面和登录页面中复用。
  2. 动态渲染:在模板中使用 Thymeleaf 的表达式和标签,动态渲染页面内容。例如,根据用户输入显示错误信息。
  3. 路径管理:使用 th:hrefth:src 标签管理静态资源的路径,确保页面代码整洁且易于维护。”

三、 会话管理

01 解释HTTP协议的无状态性,并说明为什么在Web开发中需要会话管理?

HTTP协议是无状态的,这意味着同一个连接中的多次请求之间没有关联,服务器不会记住之前的请求。这种设计使得HTTP简单且可扩展,但在实际开发中,特别是需要用户登录和保持用户状态的场景下,无状态性就带来了问题。比如,用户登录后,服务器需要记住用户的身份,以便后续请求能够识别用户并提供个性化服务。为了解决这个问题,我们引入了会话管理技术,如Cookies和Session,通过它们来在多次请求之间保持用户的会话状态,从而实现业务连续性。

02 请解释Cookie和Session的区别,并说明它们各自的适用场景?

Cookie和Session都是用于会话管理的技术,但它们的存储位置和使用场景有所不同。Cookie是存储在客户端(浏览器)的一小块数据,服务器通过响应头将Cookie发送给浏览器,浏览器在后续请求中自动携带这些数据。Cookie适合存储少量非敏感数据,比如用户的偏好设置或跟踪信息。Session则是存储在服务器端的数据,服务器通过一个唯一的Session ID来识别用户,Session ID通常通过Cookie传递给服务器。Session适合存储敏感或大量数据,比如用户的登录信息或购物车内容,因为数据存储在服务器端,安全性更高。

03 在分布式部署中,如何解决Session不共享的问题?

在分布式部署中,用户的请求可能被分发到不同的服务器上,导致Session数据无法共享。为了解决这个问题,有几种常见的解决方案:

  1. 粘性Session:通过负载均衡器将同一个用户的请求始终分发到同一个服务器上,确保Session数据一致。
  2. 同步Session:当一个服务器创建或更新Session时,将Session数据同步到其他服务器上。
  3. 共享Session:使用一个独立的服务器(如Redis)来存储所有Session数据,其他服务器通过访问这个共享服务器来获取Session数据。
  4. 数据库存储:将Session数据存储在数据库中,所有服务器都可以访问数据库来获取Session数据,实现数据共享。

04 为什么在分布式系统中推荐使用Redis来存储Session数据?

在分布式系统中,Redis被广泛用于存储Session数据,主要因为它具有以下优点:

  1. 高性能:Redis是一个内存数据库,读写速度非常快,能够满足高并发的需求。
  2. 数据共享:Redis可以作为所有服务器的共享存储,确保Session数据在不同服务器之间保持一致。
  3. 持久化:Redis支持数据持久化,即使服务器重启,Session数据也不会丢失。
  4. 灵活性:Redis支持多种数据结构,可以根据需求灵活存储和管理Session数据。

05 在Java中,如何创建一个Session并存储用户数据?

在Java中,创建Session并存储用户数据非常简单。首先,通过HttpServletRequest对象获取Session对象,如果Session不存在,服务器会自动创建一个新的Session。然后,可以通过session.setAttribute("key", value)方法将数据存储在Session中。l例如:

1
2
HttpSession session = request.getSession(); // 获取或创建Session
session.setAttribute("username", "JohnDoe"); // 存储用户数据

在后续的请求中,可以通过session.getAttribute("key")方法来读取存储的数据。例如:

1
String username = (String) session.getAttribute("username"); // 读取用户数据

这种方式使得在多次请求之间保持用户状态变得非常简单和高效。

四、验证码生成

01 验证码功能实现介绍

验证码生成是为了在登录页面中实现随机验证码功能,防止机器人暴力破解或恶意登录。通过要求用户输入验证码,可以有效区分人类用户和自动化程序,提升系统的安全性。课程推荐使用现成的工具capture,它能够快速生成验证码图片,并且配置灵活,适合在Web开发中使用。


02 capture工具介绍

capture是一个用于在服务端内存中生成验证码的工具。它能够生成包含随机字符的图片,并且支持配置图片的宽度、高度、字符数量、字符范围、字体颜色、干扰线等参数,满足不同的需求。

03 capture工具的使用步骤

  1. 导入capture包:通过Maven依赖将capture工具导入到项目中,确保相关依赖下载完成。

  2. 编写配置类:在Spring框架下,编写一个配置类,通过@Configuration注解标记为配置类,并通过@Bean注解将capture的核心对象Producer接口实例化并注入到Spring容器中。

  3. 生成验证码:通过Producer接口的createText方法生成随机字符,并通过createImage方法根据字符生成验证码图片。

    Producer接口有两个核心方法:

    • createText:生成随机的验证码字符。
    • createImage:根据生成的字符创建验证码图片,图片可以配置尺寸、颜色、干扰线等参数。
  4. 生成的验证码字符需要存储在服务端的Session中,以便在用户提交表单时进行比对验证。Session是一种跨请求的存储机制,适合存储敏感信息,如验证码、用户登录状态等。

  5. 在登录页面中,验证码图片的访问路径指向一个动态生成验证码的接口(如/capture)。通过JavaScript实现验证码的刷新功能,用户点击刷新按钮时,调用refreshCaptcha方法重新加载验证码图片。

  6. 在Spring框架中,可以通过配置类将capture工具的核心对象Producer注入到Spring容器中。然后在Controller中编写接口,通过Producer生成验证码图片,并通过HttpServletResponse对象将图片输出到浏览器。

04 请解释验证码的作用以及为什么在登录页面中使用验证码?

答案:
验证码的作用是区分人类用户和自动化程序,防止机器人暴力破解或恶意登录。在登录页面中使用验证码,可以增加攻击者的难度,确保只有真实用户才能完成登录操作,从而提升系统的安全性。

05 请描述如何使用capture工具生成验证码,并说明其核心步骤。

答案:
使用capture工具生成验证码的核心步骤如下:

  1. 导入capture包:通过Maven依赖将capture工具导入到项目中。
  2. 编写配置类:在Spring框架下,通过@Configuration@Bean注解将captureProducer接口实例化并注入到Spring容器中。
  3. 生成验证码:通过Producer接口的createText方法生成随机字符,并通过createImage方法生成验证码图片,最后将图片输出到浏览器。

06 在生成验证码时,如何确保验证码的字符能够被后续请求验证?

答案:
生成的验证码字符需要存储在服务端的Session中。当用户提交表单时,服务器可以从Session中获取之前生成的验证码字符,与用户输入的验证码进行比对,从而完成验证。


07 在分布式系统中,如何确保验证码的字符能够在不同服务器之间共享?

答案:
在分布式系统中,可以通过将验证码字符存储在共享的数据库(如Redis)中,确保所有服务器都能访问到相同的验证码数据。这种方式解决了Session不共享的问题,同时保证了验证码的一致性。


08 如何实现验证码的动态刷新功能?

答案:
通过JavaScript实现验证码的动态刷新功能。在登录页面中,为验证码图片设置一个动态的访问路径(如/capture),并通过JavaScript的refreshCaptcha方法重新加载验证码图片。每次点击刷新按钮时,调用该方法生成新的验证码并更新页面。

五、登录,退出功能实现

01 请描述登录功能的实现流程?


登录功能分为两个阶段:首次请求时返回登录页面,用户填写表单后提交二次请求。服务端首先校验验证码,从Session中取出生成的验证码与用户输入比对。验证码通过后,再检查用户是否存在、账号是否激活,最后对密码进行MD5加盐哈希,与数据库中存储的哈希值匹配。若全部通过,生成一个唯一UUID作为tijcket,记录到login_ticket表中,状态设为有效(status=0),并设置过期时间(例如12小时)。同时,将ticket通过Cookie下发到客户端,路径设置为根目录,后续请求自动携带该Cookie以维持登录状态。


02 为什么选择数据库存储登录凭证而不是Session?


Session在分布式场景中需要额外处理多节点同步问题(如Redis共享Session),而数据库存储ticket天然支持中心化查询,任何服务节点都可直接验证登录状态。此外,数据库允许精确控制凭证有效期(如“记住我”功能需长期凭证),而Session默认过期时间较为固定。从扩展性来看,未来若需改用Redis等高性能存储,只需调整数据访问层,业务逻辑无需改动。


03 数据库表login_ticket的设计思路是什么?


该表包含五个核心字段:

  • id:自增主键,唯一标识每条凭证记录。
  • user_id:外键关联用户表,指向所属用户。
  • ticket:唯一字符串(UUID),用于客户端Cookie和服务端验证。
  • status:标识凭证状态(0有效,1失效),避免物理删除,便于审计和排查问题。
  • expired:TIMESTAMP类型,记录凭证过期时间,由服务端根据业务需求动态计算(如默认12小时或“记住我”的100天)。

通过statusexpired字段组合,可高效筛选有效凭证,避免全表扫描。


04 验证码的作用是什么?如何防止验证码被绕过?


验证码主要用于防御暴力破解和机器自动登录。实现时,服务端生成验证码图片并将文本存入Session,用户提交登录请求后,立即从Session中移除验证码文本,确保单次有效性。此外,服务端会对登录接口进行限流,例如同一IP在1分钟内失败超过5次,则锁定该账号15分钟,并通过Redis记录失败次数,防止恶意尝试。


05 退出功能如何确保用户完全登出?


用户点击退出时,服务端执行两步操作:

  1. 失效凭证:根据Cookie中的ticket更新login_ticket表,将status设为1(失效)。
  2. 清除Cookie:返回响应时设置同名Cookie,Max-Age=0,覆盖浏览器原有Cookie,触发客户端立即删除。
    双管齐下确保服务端不再认可该凭证,且客户端无法再次携带它发起请求

06. 如果用户同时在多设备登录,系统如何处理?


设计上允许同一用户生成多个有效ticket,每条记录对应不同设备。退出时仅失效当前设备的ticket,其他设备仍保持登录状态。若需强制所有设备下线,可在用户修改密码时批量将其所有ticket标记为失效。


07. MyBatis中注解和XML配置如何选择?


注解适合简单SQL(如@Select("SELECT * FROM user WHERE id=#{id}")),开发效率高;XML则便于管理复杂SQL(如动态条件拼接、关联查询)。项目中login_ticket的增删改查使用注解实现,而用户分页查询等复杂逻辑通过XML配置,保持代码整洁性和可维护性。

六、登录退出功能的实现

Spring框架中如何通过拦截器(Interceptor)统一管理用户登录状态,并实现页面动态显示登录信息的功能。

每个页面的头部需要根据用户是否登录来展示不同内容(比如已登录显示头像,未登录显示注册入口)。为了避免在每个Controller重复编写登录校验代码,引入了拦截器。拦截器会在请求到达Controller前统一处理登录凭证(如从Cookie中提取ticket),验证用户身份,并将用户信息暂存到线程安全的存储中(如ThreadLocal),供后续流程使用,但是由于Web应用需要同时处理多个请求,每个请求对应独立线程。视频中通过ThreadLocal实现用户数据的线程隔离,确保不同请求间的用户信息互不干扰。例如,用户A的登录数据不会与用户B的数据冲突,即使两者同时发起请求。在请求处理完成后(Controller执行后),拦截器的postHandle方法会将用户信息添加到模型(Model)中,前端模板(如Thymeleaf)通过判断Model中的数据动态渲染页面。例如,如果用户已登录,模板会显示用户头像和昵称;未登录则展示登录按钮。

应用了Spring Email和SpringMvc中的Interceptor(拦截器),其中拦截器能拦截所有请求,能解决通用的问题,涉及的面比较广、影响的请求比较多要重点关注。权限模块主要开发了注册、登录、退出、状态(在每个页面上怎么去显示登录用户的头像、用户名等)、设置(用户头像、修改密码等)、授权(不同类型的用户访问不同的功能,使用Security实现的)、会话管理(重点需要了解Cookie、session、项目中为什么不用session(主要是考虑分布式部署Session的问题)、不用session是如何解决的问题(把数据存在Redis中,使用了ThreadLocal))等功能。

01 请说明如何利用拦截器实现登录信息的统一处理?


在项目中,我通过Spring拦截器统一处理登录状态,避免在每个Controller重复校验用户。具体来说,拦截器会在每个请求开始时,从Cookie中提取登录凭证(ticket),然后查询数据库验证凭证是否有效。如果有效,就将用户信息存储到当前线程的独立变量中(比如用ThreadLocal),这样后续业务逻辑可以直接获取用户数据。最后在请求结束时清理这些数据,确保线程安全。整个过程对业务代码无侵入,且集中管理登录状态,减少了代码冗余。


02 为什么选择ThreadLocal存储用户数据?如何避免内存泄漏?


因为每个请求对应一个独立线程,ThreadLocal可以为每个线程创建用户数据的副本,不同线程之间互不干扰,天然支持高并发场景。例如,用户A和用户B同时登录,他们的数据会分别存在各自的线程中,不会互相覆盖。
为了避免内存泄漏,我们会在请求处理完成后主动清理ThreadLocal中的数据。通常是在拦截器的最终阶段(如afterCompletion方法)调用remove()方法,确保线程池复用时不会残留旧数据。


03 拦截器的三个核心方法分别是什么?它们的执行顺序是怎样的?


拦截器的三个方法是:

  1. preHandle:在Controller处理请求前执行,比如这里我们用它校验用户登录状态。
  2. postHandle:在Controller处理完请求后、返回视图前执行,此时我们可以将用户数据添加到模型(Model)中,供前端页面展示。
  3. afterCompletion:在整个请求完成(包括视图渲染)后执行,用于资源清理,比如移除ThreadLocal中的数据。
    它们的执行顺序是:preHandle → Controller → postHandle → 视图渲染 → afterCompletion。

04 如果拦截器误拦截了静态资源(如CSS、JS文件),如何解决?


在配置拦截器时,可以通过排除路径(excludePathPatterns)指定不拦截的静态资源。例如,将/*.css/*.js添加到排除列表中,这样拦截器会跳过对这些路径的处理。这样做既能保证业务请求被拦截处理,又避免影响静态资源的正常加载。


05 用户未登录和已登录时,页面头部如何动态显示不同内容?


在拦截器中,我们会将用户信息存入Model,前端页面(如Thymeleaf或Freemarker)根据Model中是否有用户数据做条件判断。如果用户已登录,页面渲染时显示头像、用户名等;如果未登录,则展示登录和注册链接。这种逻辑完全由模板引擎处理,后端只需提供数据,实现了前后端解耦。


06 如何确保用户凭证(ticket)的安全性?


首先,凭证(ticket)本身是随机生成的唯一字符串,难以伪造。其次,我们通过Cookie下发凭证时会设置HttpOnly属性,防止JavaScript恶意窃取。同时,服务端每次收到请求都会校验ticket的有效性,包括检查状态是否失效、是否过期。即使凭证被截获,攻击者也无法在过期后使用,且服务端可以主动使token失效(如用户主动退出时)。

七、头像上传功能如何实现的

面试官:能说一下如何实现头像上传功能吗?

:好的,头像上传功能主要分为前端和后端两部分实现。

首先,前端会设计一个表单,设置 enctype="multipart/form-data",确保可以上传文件。用户通过 <input type="file"> 选择图片后,可以用 FileReader 实现图片预览,提升用户体验。然后通过 AJAXFetch API 将文件异步上传到服务器,避免页面刷新。

后端部分,我会用 MultipartFile(如果是 Spring Boot)接收上传的文件。文件存储有两种方式:一是保存到服务器的指定目录,生成唯一的文件名(比如 UUID + 时间戳)避免冲突;二是上传到云存储(如 AWS S3 或阿里云 OSS),提高文件的可靠性和访问效率。文件存储成功后,我会把路径保存到用户表的 avatar 字段中,方便后续展示。

在前端展示时,通过 <img> 标签加载用户头像,src 属性指向后端返回的路径或 URL。如果用户没上传头像,就显示默认头像。

此外,我会做一些优化,比如限制文件大小和格式,防止恶意文件上传;如果项目需要,还可以加入图片裁剪功能,确保头像显示比例一致。

总的来说,通过前后端配合,头像上传功能可以高效实现,同时兼顾用户体验和安全性。

八、登录状态检查

在面试中,你可以这样回答关于如何实现检查登录状态的问题:


01 你能解释一下如何实现检查登录状态的功能吗?

:当然可以。实现检查登录状态的功能主要是为了确保用户在访问某些敏感或需要权限的功能时,必须处于登录状态。我们通过以下几个步骤来实现这一功能:

  1. 自定义注解
    • 首先,我们定义了一个自定义注解 @LoginRequired,用于标记那些需要登录才能访问的方法。这个注解通过 @Target(ElementType.METHOD) 指定它只能用在方法上,并通过 @Retention(RetentionPolicy.RUNTIME) 确保它在运行时有效。

  2. 拦截器
    • 我们实现了一个拦截器 LoginRequiredInterceptor,它实现了 HandlerInterceptor 接口。在 preHandle 方法中,我们检查当前请求的目标方法是否带有 @LoginRequired 注解。
    • 如果方法带有该注解,我们进一步检查用户是否已经登录。我们通过 Holder 类(通常是一个单例或上下文对象)来获取当前用户信息。如果用户未登录(即 Holder.getUser() == null),我们拒绝该请求,并通过 response.sendRedirect 将用户重定向到登录页面。

  3. 注解应用
    • 在需要登录才能访问的方法上,我们添加 @LoginRequired 注解。例如,用户设置和上传头像的方法都需要登录才能访问,因此我们在这些方法上添加了该注解。

  4. 拦截器配置
    • 最后,我们将拦截器配置到 Spring MVC 的拦截器链中,确保它能够拦截所有请求,并根据注解进行相应的处理。

通过这种方式,我们确保了只有登录的用户才能访问特定的功能,从而提高了系统的安全性。

02 你能详细解释一下拦截器是如何工作的吗?

:当然可以。拦截器是 Spring MVC 提供的一种机制,允许我们在请求处理的不同阶段插入自定义逻辑。具体来说,LoginRequiredInterceptor 的工作流程如下:

  1. preHandle
    • 在请求到达控制器之前,preHandle 方法会被调用。我们在这个方法中检查请求的目标方法是否带有 @LoginRequired 注解。
    • 如果方法带有该注解,我们进一步检查用户是否已经登录。如果用户未登录,我们返回 false,并重定向用户到登录页面,阻止请求继续处理。

  2. postHandle
    • 在控制器处理完请求之后,视图渲染之前,postHandle 方法会被调用。我们在这个方法中可以进行一些后处理操作,但在这个场景中我们不需要使用它。

  3. afterCompletion
    • 在请求处理完成之后,无论成功与否,afterCompletion 方法都会被调用。我们在这个方法中可以进行一些清理操作,但在这个场景中我们也不需要使用它。

通过这种方式,拦截器能够在请求处理的不同阶段插入自定义逻辑,确保只有登录的用户才能访问特定的功能。

03 你提到使用自定义注解,你能解释一下如何定义和使用它吗?

:当然可以。自定义注解 @LoginRequired 的定义和使用如下:

  1. 定义注解
    • 我们使用 @Target(ElementType.METHOD) 指定该注解只能用在方法上。
    • 使用 @Retention(RetentionPolicy.RUNTIME) 确保该注解在运行时有效,这样我们才能在运行时通过反射读取它。

    1
    2
    3
    4
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LoginRequired {
    }
  2. 使用注解
    • 在需要登录才能访问的方法上,我们添加 @LoginRequired 注解。例如:

    1
    2
    3
    4
    @LoginRequired
    public void userSetting() {
    // 用户设置逻辑
    }
  3. 读取注解
    • 在拦截器中,我们通过反射读取方法上的 @LoginRequired 注解。如果方法带有该注解,我们进一步检查用户是否已经登录。

    1
    2
    3
    4
    LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
    if (loginRequired != null) {
    // 检查用户是否登录
    }

通过这种方式,我们能够灵活地标记哪些方法需要登录才能访问,并在拦截器中统一处理这些方法的访问控制。

九、敏感词过滤

01 如何实现的敏感词过滤功能

回答
在实现敏感词过滤功能时,我使用了前缀树(Trie树)​数据结构来高效地存储和检索敏感词。具体步骤如下:

  1. 定义前缀树:我定义了一个前缀树结构,每个节点包含一个字符和一个标记,用于表示该节点是否是一个敏感词的结尾。
  2. 初始化前缀树:我从配置文件中读取敏感词列表,并将这些敏感词逐个插入到前缀树中。每个敏感词的字符会被逐层插入到树中,形成树的分支。
  3. 敏感词过滤算法:我使用了三个指针来遍历用户输入的字符串。指针一指向前缀树的当前节点,指针二指向字符串的起始位置,指针三指向字符串的当前位置。通过遍历字符串,逐个字符在前缀树中查找匹配。如果找到匹配的字符,则继续向下查找;如果未找到匹配,则跳过该字符。当检测到一个完整的敏感词时,将其替换为预定义的符号(如“***”),并继续遍历剩余的字符串。
  4. 处理特殊符号:为了防止用户在敏感词中插入特殊符号(如“开*票”)来绕过过滤,算法会跳过这些特殊符号,继续检测后续字符。

通过这种方式,我实现了一个高效且可靠的敏感词过滤功能,能够有效地检测并替换用户输入中的敏感词汇。

02 视频功能实现的功能

在视频中,主要讲解的是如何实现一个敏感词过滤的功能。这个功能的核心目的是在用户输入的内容中检测并替换掉敏感词汇,以确保内容的合法性和安全性。具体实现步骤如下:

  1. 定义前缀树(Trie树)数据结构
    • 前缀树是一种树形数据结构,用于高效地存储和检索字符串。每个节点代表一个字符,从根节点到某个节点的路径表示一个字符串。
    • 在前缀树中,根节点不包含任何字符,其他每个节点包含一个字符。通过从根节点到某个节点的路径,可以拼接出一个完整的字符串。
  2. 初始化前缀树
    • 从配置文件中读取敏感词列表,并将这些敏感词逐个插入到前缀树中。每个敏感词的字符会被逐层插入到树中,形成树的分支。
    • 在插入过程中,如果某个字符已经存在于树中,则直接复用该节点,避免重复创建。
  3. 敏感词过滤算法
    • 使用三个指针来遍历用户输入的字符串:
      • 指针一:指向前缀树的当前节点,用于在树中查找匹配的字符。
      • 指针二:指向字符串的起始位置,用于标记疑似敏感词的开始。
      • 指针三:指向字符串的当前位置,用于遍历字符串。
    • 算法通过遍历字符串,逐个字符在前缀树中查找匹配。如果找到匹配的字符,则继续向下查找;如果未找到匹配,则跳过该字符。
    • 当检测到一个完整的敏感词时,将其替换为预定义的符号(如“***”),并继续遍历剩余的字符串。
  4. 处理特殊符号
    • 为了防止用户在敏感词中插入特殊符号(如“开*票”)来绕过过滤,算法会跳过这些特殊符号,继续检测后续字符。

十、 发布帖子

01 如何实现的发布帖子?

面试回答话术

发布帖子是简单的增删改,调用底层mapper来添加帖子,只需要判断当前用户是否登录,index页面也会做判断,当前没有用户登录时,不会显示发布新帖按钮

“在实现发布帖子功能时,我主要分为前端页面设计、异步请求发送、后端数据处理和数据库操作四个部分来实现。下面我详细说明一下:

  1. 前端页面设计
    • 我在首页设计了一个‘我要发布’按钮,点击后会弹出一个模态框。模态框中包含两个输入框,分别用于输入帖子标题和内容。底部有一个‘发布’按钮,点击后触发异步请求。
  2. 异步请求发送
    • 使用 jQuery 的 $.post() 方法发送异步请求。请求的 URL 是 /community/discuss/add,请求参数是用户输入的标题和内容,格式为 JSON 对象。在回调函数中,我处理服务器返回的 JSON 数据,并根据返回的 code 值动态提示用户发布结果。
  3. 后端数据处理
    • 后端使用 Spring Boot 框架处理请求。在 DiscussPostController 中,我定义了一个 addDiscussPost 方法,使用 @PostMapping 注解标记为 POST 请求,并通过 @ResponseBody 返回 JSON 格式数据。
    • 首先,我验证用户是否登录。如果未登录,返回 403 状态码和提示信息。如果已登录,构造 DiscussPost 对象,设置标题、内容、用户 ID 和创建时间等字段,然后调用 DiscussPostServiceaddDiscussPost 方法保存帖子。
    • DiscussPostService 中,我对标题和内容进行了敏感词过滤(使用 SensitiveFilter)和 HTML 标签转义(使用 HtmlUtils.escape),确保数据的安全性和合规性。
  4. 数据库操作
    • DiscussPostMapper 中,我定义了 insertDiscussPost 方法,使用 MyBatis 的 @Insert 注解编写 SQL 语句,将帖子数据插入 discuss_post 表。
  5. 前端处理返回结果
    • 在异步请求的回调函数中,我解析服务器返回的 JSON 数据。如果 code 为 0,提示用户发布成功并刷新页面;如果 code 为 403,提示用户未登录;其他情况则提示发布失败的具体原因。

十一、帖子详情、显示评论

01 请描述一下你实现的“帖子详情”功能。

我的回答:

“帖子详情”功能是用户在浏览帖子列表时,点击某个帖子的标题后,能够跳转到一个展示该帖子详细信息的页面。这个页面上会完整展示帖子的标题、作者、发布时间、正文等内容。

在实现这个功能时,我按照以下步骤进行开发:

  1. 数据访问层
    • Mapper接口中增加了一个根据帖子ID查询帖子详情的方法selectDiscussPostById,并在对应的XML配置文件中编写了SQL查询语句,通过WHERE id = #{id}条件来获取指定帖子的详细信息。
  2. 业务逻辑层
    • Service层中,我增加了一个findDiscussPostById方法,直接调用Mapper层的方法来获取帖子数据。由于这里没有复杂的业务逻辑,所以实现起来比较简单。
  3. 表现层
    • Controller层中,我增加了一个处理帖子详情请求的方法getDiscussPost,通过@RequestMapping注解定义了访问路径,并使用@PathVariable注解获取帖子ID。然后调用Service层的方法查询帖子数据,并将结果通过Model对象传递给前端页面。
    • 为了在页面上展示作者信息,我还通过UserService查询了帖子的作者信息,并将作者数据一并传递给前端。
  4. 前端页面
    • 在帖子列表页面上,我为每个帖子的标题添加了一个超链接,点击后会跳转到帖子详情页。链接的路径通过Thymeleaf模板引擎动态生成,确保能够正确传递帖子ID。
    • 在帖子详情页面上,我使用Thymeleafth:textth:src等标签动态渲染帖子的标题、作者头像、用户名、发布时间、正文等内容。
  5. 优化与扩展
    • 考虑到性能问题,我计划在后续引入Redis缓存,将频繁访问的帖子数据和用户信息缓存起来,减少数据库查询的开销。
    • 目前帖子详情页还没有展示回复功能,后续会结合“帖子回复”模块的开发,进一步完善帖子详情页的功能。

帖子详情是指点击首页的贴名后会跳转到该帖子的详情页,详情页会显示帖子内容,以及该帖子的评论和回复,当点击贴名发送请求时,会把所点击的帖子id传到controller中,随后调用mapper找出该帖子,传回到controller中,设置分页信息,根据帖子id查询评论列表,由于在页面中要显示该评论的作者,以及该评论和回复的各种信息,因此遍历评论列表,通过每条评论的id查询每条评论下是否有回复列表,添加帖子中controller传回页面携带的数据中除了基本的帖子信息,也有一个List,{comments},是Map<String,Object>类型,存放的是评论列表,每条记录中map里存放的是该评论的内容,作者等信息,还有回复列表,回复列表结构和评论列表一样,是list,类型为Map<String,Object>,每条map存放的是回复的相关信息。回复列表比评论列表多一个参数:目标对象,指的是当前回复回复的对象是谁。

十二、添加评论

添加评论比较重要的一点是确定好targetId目标对象id以及entityType,评论类型。如果评论是回复人的,需要设置targetId,不过targetId是当用户输入完回复信息后发送请求时直接封装到comment对象中,对象中也封装有entityType评论类型,entityId评论id。

十三、私信列表和私信详情

1.私信列表

私信列表是指打开首页并登录后显示的消息,在消息页面中,显示的是消息列表,需要查询数据库中的消息列表,并且需要显示当前用户的所有未读消息数。点开消息后,出现的是当前用户与目标用户以往发送的私信,并且当用户有未读消息时,需要提示当前未读消息数,当用户打开后未读消息数清0。

私信列表就是简单的调用底层mapper,查询属于该用户的私信时做判断,fromId==userId || toId == userId,并且查询私信列表时,只需要查询该用户根据conversation_id分组后最新的一条数据,具体的私信内容在在点开私信详情后在显示。在查询私信列表时并将该用户每条私信列表的未读数量和私信数量传入前台页面。

2.私信详情

私信详情就是点开某条具体的私信后显示当前用户和目标对象所进行的所有私信记录,查询出当前conversationId所有私信信息后,为每条消息设置信息,fromUser,主要作用是显示发送该消息的用户的头像。

十四、项目中的redis如何使用的?

01 使用场景

缓存点赞和关注:

在项目中,点赞是非常频繁的操作,如果将点赞的信息存入到数据库中,在用户量不高或者低并发的情况下,不会有情况,但如果用户多或者操作非常频繁时就会大大增大系统的压力,因此需要将点赞信息存入redis中,在nosql中存取数据比在关系型数据库中存取数据快的多。

1.点赞

编写likeService,点赞的逻辑是当用户点赞后,首先通过entityType,entityId,userId获取entityLikeKey和userLikeKey。entityLikeKey存放的是为该实体点赞的用户,userLikeKey存放的是该userId所获得的所有赞个数。当有用户为帖子或者评论点赞时,将entityType,entityId,userId传到controller层执行点赞操作,首先判断该用户是否为这个实体点过赞,如果之前点过,这次操作应该是取消点赞,同时将实体所属用户所获得的赞减一。如果没点过赞,将userId存到实体所获赞的set集合中,并将实体所属用户所获得的赞加一。这里entityuserId是指实体所属用户id,userId是当前登录的用户

2.缓存实体赞

在页面显示时,也要显示当前实体所获得的赞。并且每当当前用户打开帖子时,都要判断当前用户对这个帖子以及对所有评论的点赞状态,只需要查询实体点赞列表中是否存在当前用户id即可。(如何实现见03)

3.我收到的赞

我收到的赞用于打开个人主页后需要在页面显示该用户所有的赞,因为在点赞中已经设置点赞后将用户赞加一,所有只需要调用方法查询该用户key下的数值是多少就行。

1)Redis缓存用户点赞数用String类型,以用户ID为key,点赞时,自增,取消赞时,自减;缓存实体点赞数,set类型,用户给实体点赞时添加进列表,取消赞时则移除,最后用size统计;

​ 2)缓存粉丝列表,使用zset,存入粉丝的id和关注的时间戳,使用zCard获得粉丝数量。利用reverseRange的时间戳反向排序,按关注时间加载粉丝列表。

优化登录:

1)使用Redis缓存用户信息。将user缓存到Redis中,获取user时,先从Redis获取。取不到时,则从数据库中查询,再缓存到Redis中。因为很多界面都要用到user信息,并发时,频繁的访问数据库,会导致数据库崩溃。变更数据库时,先更新数据库,再清空缓存;

2)使用Redis缓存验证码 。原本添加到session中,减轻服务器压力。将验证码存到Redis中,方便查询检验;

  • -验证码需要频繁的访问与刷新,对性能要求很高;

  • -验证码不需要永久存储,通常在很短的时间内就会失效;

  • -分布式部署时,存在session共享问题;

3)登录凭证:原本添加到MySQL中,为减轻每次登录都去查询数据库的压力,将登录凭证ticket缓存在Redis中,防止每次都要进行数据库的查询,提高并发能力。退出登录时,原本要修改数据库中的登录凭证,现在只需要修改Redis即可。

关注:

实现关注功能时,我使用 Redis 来存储关注关系,设计了两类 Key:following:userId:entityType 存储用户关注的实体,follower:entityType:entityId 存储实体的粉丝。通过 Redis 的 ZSet 存储数据,Score 为时间戳,方便按时间排序。关注操作时,将实体 ID 添加到用户的关注目标 Key,同时将用户 ID 添加到实体的粉丝 Key,并使用 Redis 的事务确保数据一致性;取消关注时则移除相应数据。这种设计高效、可靠,支持多种实体类型,并且通过 Redis 的事务机制保证了操作的原子性。

统计统计网站UA和DAU

(见下面)

使用Redis的高级数据结构:
HyperLogLog:超级日志,统计独立整数个数。统计UA(独立访问)时,以日期为 rediskey ,将客户端IP add 到HyperLogLog中(redisTemplate.opsForHyperLogLog().add(redisKey, i);)
Bitmap:位图,比如365天的签到,只需要365/8个字节的大小。统计DAU(日活跃用户)时,以日期为 rediskey ,以用户ID作为位(在数据中的位置),用 or 操作,既可以方便的统计一段时间内的注册用户访问人数。

02 redis的key怎么设计(怎样存储的点赞、关注、缓存用户数据)?

redis的key是String类型的,编写了一个工具类来生成redis的key。key由多个单词拼接而成,中间采用冒号隔开,有的单词是固定的,有些单词是动态的。

点赞使用set类型存储,key为点赞对象,set中保存点赞人的ID
关注使用zSet类型存储,key为被关注者,set保存关注者以及关注时间为score
缓存用户数据使用Value类型,key为用userID得到的key,value为user对象(设置过期时间,且数据修改时需要清除缓存)

验证码是与user相关的,但是这里我们不能直接传入userId,因为还未登录,我们不知道用户是谁。这里传入了一个字符串owner,这是在用户访问登录页面的时候,给他发一个凭证(随机字符串),存到cookie里,用的时候从cookie内将这个owner取出来,在得到rediskey,然后获取验证码,与输入的验证码进行对比。

03 缓存点赞数如何实现

帖子和评论的赞一起存,统称为实体的赞。还需要统计用户的赞(用户的帖子和评论收到的赞的总和)。因为如果统计用户所有帖子和评论的赞得到用户获得的赞太麻烦,所以这里以用户ID采用rediskey工具拼接为key记录点赞数量(这就会涉及到事务操作。用户的帖子或者评论的点赞数增加了对应的用户的赞要增加)。

具体实现:使用redis来存储点赞数,首先需要构造redis的key,

点赞使用set类型存储,key为点赞对象,set中保存点赞人的ID

点赞的时候需要判断用户是否已经点赞:通过redistemplate.opsforSet().ismember方法 如果已经点过赞了就要把点赞记录删除 否则添加数据。 这里用到了事务操作 重写了execute方法

项目中的redis在存储用户信息时,是只读模式。

04 如何解决缓存和数据库的数据不一致问题?

缓存和数据库的数据不一致一般是由两个原因导致的,提供了相应的解决方案。

  • 删除缓存值或更新数据库失败而导致数据不一致,可以使用重试机制确保删除或更新操作成功。
  • 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删。

重试机制:具体来说,可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了。否则的话,我们还需要再次进行重试。如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了 。

延迟双删: 一般应用于先删除缓存,再更新数据库的多线程并发访问的情况。这是因为,先更新数据库值,再删除缓存值的情况下,如果线程 A 删除了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。
总结:

img

05 为什么使用了先删数据库再删缓存保证数据同步?

在我的项目中,缓存和数据库的同步策略采用了“先更新数据库,再删除缓存”的方式,而不是“先删缓存再更新数据库”,主要是基于以下几点考虑:

1. 避免数据不一致
如果先删缓存,再更新数据库,在删除缓存后、更新数据库前,可能会有其他请求读取数据。由于缓存为空,这些请求会从数据库中读取旧数据并重新加载到缓存中。即使数据库更新完成,缓存中仍然是旧数据,导致缓存与数据库不一致。而在我的项目中,数据一致性是核心需求,因此优先保证数据库更新成功后再删除缓存。

2. 减少缓存穿透风险
先删缓存后,如果更新数据库的时间较长,在这段时间内可能会有大量请求直接穿透缓存,直接访问数据库,导致数据库压力骤增。我的项目是一个高并发系统,数据库的性能至关重要,因此需要避免这种风险。

3. 简化并发控制
在高并发场景下,如果先删缓存再更新数据库,可能会出现多个请求同时删除缓存并更新数据库的情况,导致数据竞争。而在我的项目中,通过“先更新数据库,再删除缓存”的方式,可以确保数据库更新成功后,缓存被删除,后续请求会重新加载最新数据,避免并发问题。

4. 保证事务性
我的项目中,数据库更新是一个事务性操作,确保数据更新的原子性。如果先删缓存,再更新数据库,在数据库更新失败的情况下,缓存已经被删除,会导致数据不一致。而“先更新数据库,再删除缓存”可以确保数据库更新成功后,缓存被删除,即使缓存删除失败,也可以通过重试机制保证最终一致性。

5. 性能优化
先更新数据库,再删除缓存,可以确保在数据库更新成功后,缓存被及时删除,减少缓存与数据库的同步延迟,提高系统的响应速度。这对于我的项目来说,是一个重要的性能优化点。

综上所述,在我的项目中,“先更新数据库,再删除缓存”的策略能够更好地保障数据一致性,减少缓存穿透风险,简化并发控制,并优化系统性能,因此选择了这种方案。

十五、项目中哪用了kafka?

01 怎么用了?

当有点赞,评论,关注请求时,会发送系统通知点赞,评论,关注的对象。在处理系统信息时,使用到了Kafka,具体来说,先定义了生产者类和消费者类,其中生产者被点赞/评论/关注功能对应的Controller使用,产生消息。而消费者负责消息(message)到来时,把消息存到数据库内。

1. 生产者侧:用户触发事件,发消息到 Kafka
比如用户A评论了用户B的帖子,后端会生成一条JSON消息,内容包含通知类型、发送人、接收人、帖子ID这些关键信息。然后通过Kafka生产者把这条消息发到对应的Topic里,比如评论通知单独一个Topic,点赞另一个。这里有个细节,我按接收人的用户ID做了分区,保证同一个用户的通知按顺序处理,避免乱序问题。

3. 消费者侧:异步处理,存库+推送给用户
消费者服务一直监听Kafka的Topic,拉取到新消息后,主要做两件事:

  • 存数据库:把通知内容存到MySQL的message表,同时用Redis记录用户的未读通知数(比如小红点)。
  • 实时推送:如果用户在线,通过WebSocket立刻推前端;如果离线,等下次登录再拉取。

02 生产者消费者模型

生产者消费者模型

Kafka 的生产者-消费者模型是一种高效的消息传递机制,生产者负责将消息发布到指定的 Kafka 主题(Topic),而消费者则从这些主题订阅并消费消息。Kafka 通过分区(Partition)和消费者组(Consumer Group)的设计,实现了高吞吐量和可扩展性。生产者可以异步发送消息,并支持消息确认机制,确保数据可靠性;消费者组内的多个消费者可以并行处理消息,Kafka 会为每个消费者分配特定的分区,保证消息的顺序性。这种模型解耦了生产者和消费者,适用于日志收集、实时数据处理和事件驱动架构等场景

kafka入门

Apache Kafka是一个分布式流平台。一个分布式的流平台应该包含3点关键的能力:

·kafka特点

-高吞吐量:处理TB级的海量数据

-消息持久化:持久化,将数据存储到硬盘上,而不仅仅存储在内存中,长久保存消息,存到硬盘中的读取速度远远小于内存,读写硬盘的效率高低取决于读取硬盘的方式,硬盘的顺序读写的效率是很高的,kafka保证对硬盘消息的读写都是顺序的;

-高可靠性:kafka是分布式部署,一台服务器挂了,还有别的,有容错机制

-高拓展性:集群的服务器不够时,可以扩展服务器,只需简单的配置

·kafka术语

消息模型:发布-订阅模型,消费者订阅了某一主题(topic)后,生产者采用类似广播的方式,将消息通过主题传递给所有的订阅者。
Topic:主题,类似于文件夹,用来存放不同的数据。
Partition:主题分区,同一主题的不同分区可以存放在不同的Broker上面,保证并发能力和负载均衡。
Offset:消息在Partition中的存放位置。
Broker:可以理解为kafka集群里面的一台或多台服务器,它本身是没有复制的,上面可能运行着topic1的leader, topic2的follower等等。

02 消息队列放到内存还是磁盘?放磁盘为什么还这么快?

Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka的特性之一就是高吞吐率。

从数据写入和读取两方面分析,为什么Kafka速度这么快

写入数据:磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。在顺序读写的情况下,磁盘的顺序读写速度和内存持平。因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最讨厌随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。

即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘 ,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。

读取数据:实现了零拷贝,

传统数据读取的问题:传统的数据读取需要经过多次数据拷贝:

  1. 从磁盘读取数据到内核缓冲区。
  2. 从内核缓冲区拷贝到用户空间缓冲区。
  3. 从用户空间缓冲区拷贝到内核的网络缓冲区。
  4. 从网络缓冲区发送到网络设备。

Kafka 的优化:Kafka 使用零拷贝技术(如 sendfile 系统调用),直接将数据从磁盘文件传输到网络设备,跳过了用户空间的拷贝。这样减少了 CPU 和内存的开销,提高了数据读取的效率。

十六、Elasticsearch

01 Elasticsearch是什么

Elasticsearch是由 ]ava语言开发基于Lucene的一款开源的搜索、聚合分析和存储引学。同时它也可以称作是一种非关系型文档数据库。(文档型数据库,它以 文档 为基本单位存储数据。文档通常采用 JSON、BSON 或 XML 等格式,每个文档是一个自包含的数据单元,包含键值对、嵌套对象或数组等结构。文档型数据库的核心思想是将数据以更自然的方式存储,而不是强制将其拆分为表和行。)

其具备天生分布式、高性能,高可用,易拓展,易维护,跨平台的特性,广泛应用于,海量数据的全文检索,搜索引擎,站内搜索等,还有日志系统ELK的使用也可以参与。

概念:ES是一个基于lucene构建的,分布式的,RESTful的开源全文搜索引擎。
存储原理:数据按照Index – Type – Document – 字段四级存储,其中Index对应数据库,Type对应表,Document为搜索的原子单位,包含一个或多个容器,基于JSON表示。字段是指JSON中的每一项组成,类似于数据库中的行/列。Mapping是文档分析过滤后的结果,根据用户自定义,将某些文字过滤掉,类似于表结构定义DDL??。同时ES也和分布式数据库一样,支持shard的replication。
功能:
1、分布式的搜索引擎和数据分析引擎
2、全文检索,结构化检索,数据分析。
3、对海量数据进行近实时的处理
特点:
1、可以作为分布式集群处理PB级别的数据,也可单机使用。
2、不是特有技术,而是将分布式+全文搜索(lucene) + 数据分析合并在一起。
3、操作简单,作为传统数据库的补充,提供了数据库所不具备的很多功能。

02 ES中的mapping是什么,ES的数据类型有哪些?

ES中的mapping有点类似于与RDB中“表结构”的概念,在MySQL中,表结构里包含了字段名称,字段的类型还有索引信息等。在Mapping里也包含了一些属性,比如字段名称、类型、字段使用的分词器、是否评分、是否创建索引等属性,并且在ES中一个字段可以有多个类型。分词器、评分等念在后面的课程讲解。

常见类型:

  1. 数字类型:long integer short byte double float half_float scaled_float unsigned_long

  2. Keyword类型:适用于索引结构化的字段,可以用于过滤、排序、聚合。keyword类型的字段只能通过精确值(exact value)搜索到。id应该用keyword。keyword字段通常用于排序,汇总和Term查询,例如term

还有两个不常见的类型:

  • constant_keyword:始终包含相同值的关键字字段
  • wildcard:可针对类似grep的通配符查询优化日志行和类似的关键字值
  1. dates(时间类型):

包括date和date_nanos

  1. alias:为现有字段定义别名
  2. **text:**当一个字段是要被全文搜索的,比如Email内容、产品描述,这些字段应该使用text类型,设置text类型以后,字段内容会被分析,在生成倒排索引以前,字符串会被分析器分成一个一个词项。text类型的字段不用于排序,很少用于聚合。主要原因如下:
  • 分词问题:text 字段的内容会被分词,排序和聚合需要直接访问字段的原始值,而分词后的值无法直接用于这些操作。
  • 内存占用高:如果为 text 字段创建正排索引(用于排序和聚合),会占用大量堆空间,尤其是高基数字段(字段值非常多且不重复)。这会导致内存压力增加,甚至可能引发性能问题。
  • 加载成本高:加载正排索引是一个昂贵的操作,尤其是在字段值非常多的情况下,这会导致查询延迟增加,影响用户体验。
  • 生命周期长:正排索引一旦加载到内存中,就会在整个段的生命周期内驻留在内存中,即使不再使用,也不会被立即释放,进一步加剧内存压力。

为了解决这个问题,通常的做法是:

  • 如果字段的值是短文本且不需要分词(如标签、状态码等),可以将其映射为 keyword 类型,keyword 类型字段会创建正排索引,支持排序和聚合。
  • 如果需要同时支持全文搜索和排序/聚合,可以使用多字段映射(Multi-fields),例如将一个字段同时映射为 text 和 keyword 类型,分别用于全文搜索和排序/聚合。

通过这种方式,Elasticsearch 在保证全文搜索性能的同时,避免了不必要的资源消耗。”

追问场景示例

面试官:你能举一个实际项目中如何使用多字段映射的例子吗?
候选人
“在电商项目中,我们有一个商品描述字段 description,需要同时支持全文搜索和排序。我们使用了多字段映射,将 description 字段映射为 text 和 keyword 类型:

  • description 字段用于全文搜索,支持分词和模糊查询。
  • description.keyword 字段用于排序和聚合,存储原始值,支持精确匹配。

例如,用户可以通过 description 字段搜索包含‘舒适’关键词的商品,同时通过 description.keyword 字段对商品描述进行字母顺序排序。这种方式既满足了全文搜索的需求,又支持了排序和聚合操作。”

03 项目中哪里使用到了ES,如何使用

在进行帖子搜索时,使用到了ES。可用Repository和Template两种方式,由于Repository搜索到的结果(直接返回的post类,方便)没有高亮标签(why),所以使用了template方式重写了mapResults函数,获得了带有高亮标签的post。
使用消息队列(kafka)的方式,实现发帖/删帖后ES数据库的自动更新。
搜索:定义SearchQuery,确定搜素内容,排序方式,高亮等。接着使用elasticTemplate.queryForPage方法,需要重写mapResults函数,得到高亮数据。

十七、面经

01 如何分析优化SQL执行效率?

  1. 使用 EXPLAIN 分析执行计划:通过 EXPLAIN 命令查看 SQL 语句的执行计划,重点关注是否使用了索引、是否有全表扫描、连接类型(如 JOIN)是否高效等。通过分析执行计划,可以找到 SQL 的性能瓶颈。

  2. 优化索引:确保查询条件中的字段有合适的索引,尤其是高频查询的字段。避免在索引列上使用函数或计算,这会导致索引失效。同时,注意避免创建过多索引,因为索引会增加写操作的开销。

  3. 避免全表扫描:全表扫描会显著降低查询性能,应尽量通过索引或优化查询条件来避免。例如,使用 WHERE 条件筛选数据,或通过分页查询减少返回的数据量。

  4. 优化查询语句

    • 避免使用 SELECT *,只查询需要的字段,减少数据传输量。
    • 减少子查询的使用,尽量用 JOIN 替代。
    • 避免在 WHERE 条件中使用 OR,尤其是在不同字段上,这可能导致索引失效。
  5. 利用缓存:对于高频查询且数据变化不频繁的场景,可以使用 Redis 等缓存技术,减少数据库的访问压力。

02 EXPLAINtype 字段的可能值及含义?

type 字段表示 MySQL 访问表的方式,常见的值及其含义如下:

  • system:表只有一行数据(系统表),是性能最高的访问方式。
  • const:通过主键或唯一索引查询,返回一行数据。
  • eq_ref:使用唯一索引进行关联查询,返回一行数据。
  • ref:使用非唯一索引进行查询,返回多行数据。
  • range:使用索引进行范围查询(如 BETWEEN><)。
  • index:全索引扫描,遍历整个索引树。
  • ALL:全表扫描,性能最差,应尽量避免。