服务端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 00:24

Cypress 测试隔离和数据管理怎么做?

Cypress 默认每个 it 块前会重置浏览器状态(清 cookie、localStorage、sessionStorage),Cypress 12+ 开启 testIsolation: true 后更强——每次测试前自动 cy.visit() 恢复初始页面。数据管理分三层:fixtures 管理静态数据、cy.request() + 自定义命令做动态数据创建、cy.task() 操作数据库清理。核心原则:测试不依赖执行顺序,每个测试自给自足。追问cy.session() 怎么用?和 beforeEach 中登录有什么区别?cy.session() 缓存登录后的 cookie 和 localStorage,同一 spec 内重复使用时不重新登录,显著加速测试。而 beforeEach 每次都执行完整登录流程。session 在 spec 间不共享(Cypress 12+ 的限制),跨 spec 需配合 cy.request 预置状态。testIsolation: true 和 false 各适合什么场景?true(默认)适合功能测试,保证每个用例干净状态;false 适合需要跨测试保持状态的端到端流程测试(如多步向导),但需手动在 beforeEach 中清理关键状态。fixtures 和 cy.task() 生成数据怎么选?fixtures 适合不变的测试输入(表单数据、API 响应模板);cy.task() 适合需要与后端交互的动态数据(创建测试用户、插入数据库记录),task 在 Node 环境执行,能直连数据库。如何保证并行执行时测试数据不冲突?用唯一标识生成数据:Date.now() 或 Cypress._.random(),避免固定用户名。测试结束在 afterEach 中通过 cy.request 或 cy.task 清理自己创建的数据,不依赖全局 reset。写段代码// cy.session 加速登录 + fixtures 管理数据Cypress.Commands.add('login', (role) => { cy.session(role, () => { cy.fixture('users').then((u) => { cy.request('POST', '/api/login', u[role]); }); });});
服务端阅读 05月29日 00:24

JWT 如何满足 GDPR 等合规性要求?

JWT 与合规的核心矛盾:JWT 不可变(签发后不能修改内容),但 GDPR 要求用户可以删除数据(被遗忘权)。解决思路有三个:一是 payload 只放最小必要信息(用户 ID + 角色),不存个人数据;二是实现 token 黑名单或短期 TTL,用户请求删除时立即作废所有 token;三是审计日志记录所有 token 签发和使用,满足可追溯要求。对 HIPAA/PCI DSS 同理——JWT 中绝不放 PHI(受保护健康信息)和支付卡号,只存引用 ID。## 追问GDPR 被遗忘权与 JWT 不可变性怎么协调?JWT 内容无法修改,但可以通过作废来"遗忘":将用户所有 token 加入黑名单、使 refresh token 失效、删除服务端用户数据。过期 token 中的个人信息残留属于技术限制,GDPR 在合理范围内允许。HIPAA 对 JWT 有什么特殊要求?不能在 payload 中存储任何 PHI(诊断、药物、病历等),只放用户 ID 和 scope 声明。所有 PHI 访问必须记录审计日志(谁在什么时间访问了什么数据),JWT 中的 sub 和 scope 用于日志关联。密钥轮换与合规审计有什么关系?合规框架(SOC 2/ISO 27001)要求定期轮换密钥。实现方式:维护多个密钥版本,新 token 用新密钥签发,验证时按 kid 字段选择对应密钥。轮换期间旧 token 逐步过期,无需强制用户重新登录。JWT 的审计日志应该记录什么?至少包含:用户 ID、操作类型(签发/验证/撤销)、时间戳、来源 IP、User-Agent、token 前缀(不记录完整 token)。日志存储到不可篡改的系统(如 append-only 存储),满足 SOC 2 审计要求。## 写段代码javascript// GDPR 合规:数据删除 + token 作废async function deleteUserData(userId) { // 作废所有 token await redis.set(`deleted:${userId}`, '1', 'EX', maxTokenTTL); // 删除用户数据 await db.delete('users', { id: userId }); // 审计日志 await auditLog('DATA_DELETED', { userId, reason: 'user_request' });}// 验证时检查用户是否已删除if (await redis.get(`deleted:${decoded.sub}`)) { throw new Error('user data deleted');}
服务端阅读 05月29日 00:24

Cypress 和 Selenium 有什么核心区别?

Cypress 直接运行在浏览器内部,通过 Chrome DevTools API 与页面通信,无需 WebDriver 中间层;Selenium 通过外部 WebDriver 进程以 HTTP 协议控制浏览器,架构上多了一跳延迟。Cypress 自动重试断言、内置时间旅行调试、仅支持 Chromium 系浏览器;Selenium 支持所有主流浏览器但需手动处理显式等待。选择依据:前端 SPA 项目选 Cypress 快速反馈,跨浏览器兼容测试选 Selenium。追问Cypress 的自动等待机制和 Selenium 的显式等待有什么区别?Cypress 在断言失败时自动重试(默认 4 秒),无需手动写 wait;Selenium 必须用 WebDriverWait 显式等待元素出现,否则直接抛异常。Cypress 为什么不支持跨域和多标签页?Cypress 运行在同源策略下,跨域需 cy.origin() 处理,多标签页通过模拟而非真正打开新窗口,这是架构上的硬限制。Selenium Grid 和 Cypress Cloud 的并行策略有何不同?Selenium Grid 自建节点分发测试到不同浏览器,免费可控;Cypress Cloud 依赖官方服务按机器数并行,免费版有限制。两者在 CI/CD 中如何选择?小团队前端项目用 Cypress 开发体验好、上手快;大型项目需 Firefox/Safari 兼容性验证时,Selenium 更合适,也可混合使用。写段代码// Cypress: 自动等待,无需显式 waitcy.get('#username').type('user');cy.get('#password').type('pass');cy.get('button').click();cy.url().should('include', '/dashboard');// Selenium: 必须显式等待WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, 'username')));
服务端阅读 05月29日 00:24

Cypress 视觉回归测试怎么做?

Cypress 本身不内置视觉回归功能,需借助插件实现:cypress-image-diff(轻量免费)、Percy/Chromatic(云端对比平台,付费)。核心流程是首次运行生成基准截图,后续运行做像素级 diff,差异超过阈值则判定失败。关键配置是 threshold 容忍度和动态内容排除策略。追问cypress-image-diff 和 Percy 怎么选?cypress-image-diff 本地运行、零费用、适合小团队,截图存仓库;Percy 提供云端可视化审阅、多浏览器快照、PR 集成审批流,适合中大型团队。Percy 还能自动处理抗锯齿和字体渲染差异。动态内容(日期、轮播图)导致误报怎么处理?三种策略:1) 截图前用 CSS 隐藏动态区域 cy.get('.carousel').invoke('css', 'visibility', 'hidden');2) 插件的 ignore 区域配置;3) 用 cy.clock() 冻结时间,使时间戳固定。threshold 阈值怎么设定?像素级对比用 0.01-0.05(严格),感知对比用 0.1-0.2(宽松)。建议核心页面 0.01,次要页面 0.1。首次跑测试建立 baseline 后再微调。CI 环境中截图不一致怎么解决?CI 和本地渲染差异(字体、GPU)导致误报。方案:1) Docker 统一运行环境;2) 只在 CI 中做视觉测试;3) 用 Percy 等云端工具消除本地差异;4) 禁用动画和字体反锯齿。写段代码// cypress-image-diff 基本用法cy.compareSnapshot('homepage', 0.02);// 排除动态区域cy.get('.live-data').invoke('css', 'visibility', 'hidden');cy.compareSnapshot('dashboard', 0.05);
服务端阅读 05月29日 00:23

JWT 在生产环境有哪些常见问题?怎么解决?

JWT 生产环境最核心的问题是三个:token 无法主动撤销(签发后到过期前一直有效)、payload 明文可读(Base64 非加密,敏感信息暴露)、token 体积大影响性能(每次请求都要传 1-2KB)。对应解法:撤销用黑名单(Redis 存已注销 token,TTL 等于剩余过期时间)或短期 access token + refresh token 轮换;敏感数据只放引用 ID,详情查库;体积优化用短字段名和 ES256 算法(比 RS256 小约 50%)。## 追问token 泄露了怎么办?XSS 可窃取 localStorage 中的 token,CSRF 可利用 Cookie 中的 token。防护:优先用 HttpOnly + SameSite Cookie 存储,配合 CSRF Token;access token 设短过期时间(15 分钟),泄露窗口小;检测到异常时强制刷新 refresh token 使旧 access token 自然失效。Refresh Token 轮换是什么?每次用 refresh token 换新 access token 时,同时签发新 refresh token 并作废旧的那个。如果检测到旧 refresh token 被复用,说明泄露,立即撤销该用户所有 refresh token,强制重新登录。JWT 验签每次都要算 HMAC,性能怎么优化?对称算法验签很快(微秒级),一般不是瓶颈。非对称算法(RS256)较慢,可切换到 ES256。大规模场景可在 API 网关层做验签,避免业务服务重复计算。多设备登录怎么管理?每个设备签发独立 refresh token,服务端记录设备信息。踢下线时删除对应 refresh token,该设备 access token 过期后无法续期,自然登出。## 写段代码javascript// 黑名单撤销 + refresh token 轮换async function logout(token) { const { exp } = jwt.decode(token); await redis.setex(`bl:${token}`, exp - now(), '1');}async function refresh(oldRt) { if (await redis.get(`rt:${oldRt}`)) { await redis.del(`rt:${oldRt}`); const newRt = crypto.randomBytes(40).toString('hex'); await redis.setex(`rt:${newRt}`, 7*86400, userId); return { accessToken, refreshToken: newRt }; } throw new Error('invalid refresh token');}
服务端阅读 05月29日 00:23

Cypress 要不要用 Page Object Model?

Cypress 官方并不推荐传统 POM——自定义命令和 App Actions 模式比 POM 更契合 Cypress 的命令链机制。POM 把 DOM 选择器封进类方法,但 Cypress 的重试机制使得类方法中返回 this 的链式调用容易丢失重试上下文。真正需要时,可用轻量页面对象仅封装选择器常量,操作逻辑仍交给自定义命令。追问Cypress 官方推荐的替代模式是什么?App Actions:通过 cy.request() 直接调用 API 设置状态,跳过 UI 操作步骤。例如登录不再走页面填写表单,而是 cy.request('POST', '/api/login', credentials) 配合 cy.session() 缓存。POM 在什么场景下仍有价值?页面交互极其复杂、选择器频繁变更的 SPA 项目中,POM 的选择器集中管理仍有意义。但应避免在 POM 方法中使用 Cypress 命令,改为返回选择器字符串供测试中组合。POM 方法中 cy.get() 返回 this 为什么有问题?cy.get() 返回 Chainable 而非页面对象实例,在 POM 方法中 return this 会导致后续 .should() 等断言脱离 Cypress 重试队列。正确做法是方法内完成全部操作,不返回 this 继续链式调用。选择器管理有没有更好的方案?用 data-cy 或 data-testid 属性统一选择器策略,配合自定义命令封装常用操作,比 POM 类更轻量且不丢失重试能力。写段代码// 推荐:选择器常量 + 自定义命令const selectors = { email: '[data-cy=email]', submit: '[data-cy=submit]' };Cypress.Commands.add('login', (email, pwd) => { cy.get(selectors.email).type(email); cy.get(selectors.submit).click();});
服务端阅读 05月29日 00:23

JWT 有哪些安全风险?如何防护?

JWT 的 payload 仅做 Base64 编码而非加密,任何持有 token 的人都能解码查看内容;存储在 localStorage 中的 token 易遭 XSS 窃取;攻击者可将算法篡改为 none 绕过签名验证。核心防护策略:使用短期 Access Token + 长期 Refresh Token 机制;token 存入 HttpOnly Cookie 并设置 SameSite 防 CSRF;服务端固定签名算法拒绝 alg:none;敏感数据使用 JWE 加密传输。追问JWT 无法主动撤销怎么办?设置 15-30 分钟短过期时间,配合 Redis 黑名单或 token 版本号机制,关键操作强制二次验证。HttpOnly Cookie 和 localStorage 存 token 各有什么利弊?HttpOnly 防 XSS 但需额外防 CSRF(SameSite 属性);localStorage 不防 XSS 但天然免疫 CSRF,取舍取决于项目威胁模型。RS256 和 HS256 该选哪个?RS256 非对称签名,公钥验签私钥签发,适合分布式系统;HS256 对称签名密钥需共享,密钥泄露风险更高,优先选 RS256。如何防止 token 重放攻击?在 payload 中加 jti(唯一 ID)声明,服务端记录已用 jti 拒绝重复请求,配合短过期时间降低窗口期。写段代码// 短期 Access Token + Refresh Token 签发const accessToken = jwt.sign( { sub: userId, jti: uuid() }, RSA_PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '15m' });const refreshToken = jwt.sign( { sub: userId, type: 'refresh' }, REFRESH_SECRET, { expiresIn: '7d' });
服务端阅读 05月29日 00:23

JWT 和 OAuth2.0 是什么关系?有什么区别?

它们不在同一层面:JWT 是令牌格式标准(RFC 7519),定义 token 的结构和编码方式;OAuth2.0 是授权框架(RFC 6749),定义授权流程和角色关系。一句话:OAuth2.0 规定了"怎么授权",JWT 规定了"令牌长什么样"。两者可以组合——OAuth2.0 的 Access Token 可以采用 JWT 格式,但不是必须的。OAuth2.0 也可以用不透明的随机字符串做 token。## 追问OAuth2.0 不用 JWT 行不行?完全可以。OAuth2.0 对 token 格式没有约束,可以用随机字符串(opaque token),验证时需查库。用 JWT 的好处是资源服务器可以本地验签、无需回调授权服务器,降低延迟和耦合。OAuth2.0 的四种授权模式分别适合什么场景?授权码模式(Authorization Code)适合有后端的 Web 应用,最安全;客户端凭证模式(Client Credentials)适合服务间调用;资源所有者密码模式已不推荐;隐式模式(Implicit)因安全风险已被 OAuth2.1 移除,改用 PKCE 增强的授权码模式。既然 JWT 可以独立做认证,为什么还需要 OAuth2.0?JWT 解决的是单系统的 token 格式问题。OAuth2.0 解决的是第三方委托授权问题——"让用户授权 A 应用访问 B 服务的资源,且不暴露密码"。这是 JWT 独自做不到的。OAuth2.0 用 JWT 做 token 时,刷新令牌也用 JWT 吗?一般不用。Refresh Token 需要可撤销,通常用 opaque token 存在服务端。JWT 格式的 refresh token 撤销困难,违背 refresh token 的设计初衷。## 写段代码javascript// OAuth2.0 授权码流程中签发 JWT access tokenconst accessToken = jwt.sign( { sub: userId, scope: 'read write', client_id: appId }, PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '1h' });const refreshToken = crypto.randomBytes(32).toString('hex');await redis.set(`rt:${refreshToken}`, userId, 'EX', 7*86400);
服务端阅读 05月29日 00:23

Cypress 怎么拦截和模拟网络请求?

用 cy.intercept() 拦截匹配规则的 HTTP 请求,配合 .as() 别名和 cy.wait('@alias') 实现请求等待与断言。intercept 可返回固定 stub 响应、动态构造响应、修改请求头或延迟响应,让测试脱离真实 API 依赖。注意 intercept 必须在请求发起前注册,否则无法捕获。追问cy.intercept() 和已废弃的 cy.route() 有什么区别?route 只能拦截 XMLHttpRequest,intercept 同时支持 XHR 和 Fetch API。intercept 使用 RouteMatcher 对象匹配请求(支持 method、url、headers 等多维度),功能远超 route。Cypress 6+ 已弃用 route。怎么 stub 一个带动态参数的请求?用函数式 handler:cy.intercept('GET', '/api/users*', (req) => { req.reply({ body: mockData[req.query.page] }); }),根据 req.query 或 req.body 动态构造响应。如何模拟网络错误和超时?cy.intercept('GET', '/api/data', { forceNetworkError: true }) 强制网络错误;cy.intercept('GET', '/api/data', { delay: 3000, statusCode: 200, body: {} }) 模拟超时或慢响应。多个 intercept 匹配同一请求时哪个生效?按注册顺序,最后注册的优先。建议用精确匹配规则(url + method + headers)避免冲突,或用 .as() 明确指定等待哪个。写段代码cy.intercept('GET', '/api/users*', (req) => { req.reply({ statusCode: 200, body: { users: [] } });}).as('getUsers');cy.visit('/users');cy.wait('@getUsers').its('response.statusCode').should('eq', 200);
服务端阅读 05月29日 00:23

什么是 JWT?它由哪三部分组成?

JWT(JSON Web Token,RFC 7519)是一种紧凑的自包含令牌格式,用于在各方之间安全传输信息。它由三部分组成,用点号分隔:Header.Payload.Signature。Header 指定令牌类型和签名算法(如 HS256);Payload 承载声明(Claims),包括注册声明(iss/exp/sub)、公共声明和私有声明;Signature 用 Header 指定的算法对前两部分签名,保证完整性。注意 Payload 只是 Base64 编码而非加密,不能放敏感数据。## 追问JWT 的 Signature 是怎么生成的?将 Base64Url 编码的 Header 和 Payload 用点号拼接,再用 Header 中声明的算法和密钥进行签名。公式:HMACSHA256(base64(header) + '.' + base64(payload), secret)。接收方用同一密钥重新计算并比对,即可判断 token 是否被篡改。注册声明中的 exp、iat、nbf 各是什么?exp(Expiration Time)过期时间,iat(Issued At)签发时间,nbf(Not Before)生效时间。服务端验证时会检查这些字段,过期的 token 直接拒绝。HS256 和 RS256 有什么区别?HS256 是对称加密,签发和验证用同一个密钥,适合单服务场景;RS256 是非对称加密,私钥签发、公钥验证,适合微服务架构——各服务只需公钥即可验签,无需暴露私钥。JWT 和 JWE 有什么关系?JWT 只保证完整性(防篡改),不保证机密性。JWE(JSON Web Encryption)在 JWT 基础上加密 payload,实现机密性。需要保护敏感数据时用 JWE,一般场景 JWT 足够。## 写段代码javascript// 签发与验证const token = jwt.sign( { sub: 'u123', role: 'admin', iat: Math.floor(Date.now()/1000) }, SECRET, { expiresIn: '1h', algorithm: 'HS256' });const decoded = jwt.verify(token, SECRET);
服务端阅读 05月29日 00:23

Cypress 自定义命令怎么创建和复用?

通过 Cypress.Commands.add() 在 cypress/support/commands.js 中注册自定义命令,将重复操作封装为可链式调用的 cy.xxx() 方法。自定义命令返回 cy 对象,与内置命令行为一致,支持重试和超时机制。定义时注意命名唯一、避免与内置命令冲突,复杂逻辑优先用普通工具函数而非自定义命令。追问Cypress.Commands.add() 的第二个参数支持哪些选项?可传入配置对象 { prevSubject: 'element' } 使命令接收前一个命令的 subject,实现类似 cy.get('input').myCommand() 的链式用法。prevSubject 可选 'optional'、'required'、'noop' 等。自定义命令和普通工具函数怎么选?需要重试、超时、命令日志或链式调用时用自定义命令;纯数据处理、复杂条件逻辑用普通函数。过度使用自定义命令会导致命令日志噪音和调试困难。如何覆盖已有命令?用 Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 扩展内置命令行为,如自动添加认证 header。慎用,会全局影响。自定义命令中如何正确处理异步?命令内部必须使用 Cypress 命令(cy.get、cy.request 等)而非原生 Promise,否则无法重试。如需返回值,用 .then() 提取。写段代码// cypress/support/commands.jsCypress.Commands.add('login', (email, password) => { cy.session([email, password], () => { cy.request('POST', '/api/login', { email, password }); });});// 测试中使用cy.login('user@test.com', 'pass123');
服务端阅读 05月29日 00:23

JWT 和 Session 认证有什么区别?各适合什么场景?

核心区别在于状态存储位置:JWT 是无状态的,用户信息编码在 token 中由客户端持有;Session 是有状态的,用户信息存在服务端,客户端只拿一个 Session ID。这意味着 JWT 天然支持水平扩展(任何节点都能验签),但一旦签发就无法主动撤销;Session 可以随时销毁,但多节点需要共享存储(如 Redis)。选择依据:分布式系统、移动端、跨域 API 选 JWT;需要即时吊销权限、传统 Web 应用选 Session。## 追问JWT 无法主动撤销,用户登出怎么办?常用三种方案:黑名单(Redis 存已撤销 token,过期自动清除)、短期 access token + refresh token 轮换、token 版本号(用户表加 version 字段,签发时写入,验证时比对)。JWT 存 localStorage 还是 Cookie?各有利弊。localStorage 容易被 XSS 窃取;HttpOnly Cookie 防 XSS 但需额外防 CSRF。生产推荐 HttpOnly + SameSite Cookie + CSRF Token 双重防护。Session 在微服务下怎么共享?用 Redis 集群做集中式 Session 存储,或用 Spring Session 等框架透明化处理。本质是把有状态存储从本地内存移到分布式缓存。JWT 的 payload 能放敏感信息吗?不能。Payload 只是 Base64 编码,不是加密,任何人都能解码。敏感数据只存引用 ID,详情查库获取。## 写段代码javascript// 短期 access token + 长期 refresh tokenconst accessToken = jwt.sign( { sub: userId, role }, SECRET, { expiresIn: '15m' });const refreshToken = crypto.randomBytes(40).toString('hex');await redis.setex(`refresh:${userId}`, 7*86400, refreshToken);
服务端阅读 05月29日 00:09

Python 多线程和多进程有什么区别?GIL 对多线程有什么影响?

核心区别:进程是资源分配单位,有独立内存空间;线程是调度单位,共享进程内存。在 Python 里这道题的特殊之处是 GIL——全局解释器锁让同一时刻只有一个线程执行 Python 字节码,所以 CPU 密集型任务用多线程不会加速,必须用多进程。I/O 密集型任务多线程够用,因为等 I/O 时 GIL 会释放。追问GIL 到底锁的是什么?为什么不能去掉?GIL 保护的是 CPython 的内存管理——引用计数。CPython 的对象引用计数不是线程安全的,如果多个线程同时修改引用计数,对象可能被提前释放或泄漏。加锁是最简单的方案,代价是多线程无法真正并行。Python 3.13 开始实验性支持 free-threaded 模式(PEP 703),试图移除 GIL,但目前生态兼容性还是问题。短期内 GIL 不会消失。多线程既然不能并行,还有用吗?有用。线程等待 I/O(网络请求、文件读写、数据库查询)时会释放 GIL,其他线程可以执行。所以 I/O 密集型场景(爬虫、API 调用、数据库操作)用多线程完全没问题。实测:100 个 HTTP 请求,单线程串行 10 秒,10 线程并发约 1.2 秒,接近 10 倍加速。但如果是纯计算(比如大矩阵运算),10 线程和 1 线程耗时几乎一样,甚至更慢——线程切换本身有开销。什么时候用多进程?有什么坑?CPU 密集型任务:数据处理、图像处理、机器学习训练。multiprocessing.Pool 是最常用的接口。最大的坑是进程间通信开销——进程不共享内存,数据要序列化传输。传一个大 numpy 数组给子进程,pickle 序列化的时间可能比计算本身还长。解决方案:用 multiprocessing.shared_memory(Python 3.8+)或 multiprocessing.Array 共享内存,避免序列化。另一个坑是子进程的异常不会自动传播到主进程,需要手动处理。协程和多线程有什么区别?协程是用户态的协作式调度,线程是内核态的抢占式调度。协程的切换由代码控制(await),线程的切换由操作系统控制。协程没有锁的问题——同一个时刻只有一个协程在执行,不存在竞态条件。代价是协程里不能有阻塞调用,一阻塞整个事件循环就卡住了。asyncio 适合高并发 I/O(上万连接的聊天服务器),threading 适合少量 I/O 并发或需要兼容阻塞库的场景。写段代码from multiprocessing import Poolimport timedef heavy_compute(n): return sum(i * i for i in range(n))if __name__ == '__main__': # 多进程:4核并行,接近4倍加速 with Pool(4) as p: results = p.map(heavy_compute, [10**7] * 4) print(results) # 对比:多线程对CPU密集型无效 # 4个线程和1个线程耗时几乎一样(GIL)
服务端阅读 05月29日 00:07

Python 描述符是什么?数据描述符和非数据描述符优先级怎么排?

描述符是实现了 __get__、__set__、__delete__ 中任意一个的类,被赋值给另一个类的类属性后,会拦截那个属性的访问。Python 的属性查找有一套隐藏规则:当解释器在类(及其 MRO)的 __dict__ 里找到的值是描述符时,不会直接返回它,而是调用描述符的 __get__ 方法。这就是 property、classmethod、staticmethod 的底层原理——它们都是描述符。追问数据描述符和非数据描述符有什么区别?优先级怎么排?关键区别是有没有 __set__。实现了 __get__ + __set__ 的叫数据描述符,只有 __get__ 的叫非数据描述符。优先级:数据描述符 > 实例 __dict__ > 非数据描述符。换句话说,数据描述符能拦截赋值操作,实例 __dict__ 里写不进去;非数据描述符拦截不了,一旦实例 __dict__ 有了同名 key 就被覆盖了。这就是为什么 property(数据描述符)设了 setter 后 obj.x = 1 一定走 setter,而普通方法(非数据描述符)可以被实例属性遮蔽。Python 属性查找的完整顺序是什么?按这个顺序:1. 类及其 MRO 的 __dict__ 里找,如果是数据描述符就调 __get__ 返回;2. 实例 __dict__ 里找;3. 回到类的 __dict__,如果是非数据描述符就调 __get__ 返回。这解释了一个经典面试题:为什么实例能覆盖普通方法但覆盖不了 property?因为方法是非数据描述符,步骤 2 的实例 __dict__ 优先级更高;property 是数据描述符,步骤 1 就截走了。set_name 是什么?为什么需要它?Python 3.6 新增的钩子。描述符被赋值到类属性时,解释器自动调用 desc.__set_name__(owner, name),把属性名传进去。之前描述符不知道自己叫什么名字,要么手动传(age = Typed('age', int)),要么用元类扫描类 __dict__ 来推断。有了 __set_name__,Django ORM 的 name = CharField() 就不用重复写字段名了——CharField.__set_name__ 会自动收到 'name'。描述符里怎么存值?为什么不能直接用 self.xxx?描述符实例是类级别的,所有实例共享同一个描述符对象。如果你在 __set__ 里写 self.value = val,所有实例共享同一个 value,后面的赋值会覆盖前面的。正确做法是存到 obj.__dict__[self.name] 里,或者用 weakref.WeakKeyDictionary 做 self → value 的映射。前一种更常见(property 就这么做的),后一种适合描述符本身需要维护额外状态的场景。写段代码# 用 __set_name__ 实现类型检查描述符class Typed: def __init__(self, expected_type): self.expected_type = expected_type def __set_name__(self, owner, name): self.name = name # 自动获取属性名 self.storage = f'_{name}' def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, self.storage, None) def __set__(self, obj, value): if not isinstance(value, self.expected_type): raise TypeError(f'{self.name} 需要 {self.expected_type.__name__}') setattr(obj, self.storage, value)class User: name = Typed(str) age = Typed(int)u = User()u.name = "Alice" # OKu.age = 25 # OK# u.age = "25" # TypeError: age 需要 int
服务端阅读 05月29日 00:06

Python 列表推导式和生成器表达式有什么区别?什么时候该用哪个?

区别就一个字:方括号 [] 立即算出所有结果放进列表,圆括号 () 返回一个生成器对象,用到哪个才算哪个。[x**2 for x in range(10)] 执行完内存里就有 10 个数;(x**2 for x in range(10)) 执行完只多了一个 200 字节的生成器对象,值还没算。追问生成器只能遍历一次,踩过坑吗?这是最常见的陷阱。你写 gen = (x for x in range(5)),第一次 list(gen) 得到 [0,1,2,3,4],再 list(gen) 就是 []——生成器耗尽了。如果后面的代码还要用,要么转成列表存起来,要么重新创建生成器。调试时这个坑尤其烦人:你在调试器里 print(list(gen)) 看了一眼,后面代码就拿不到数据了。sum(x2 for x in range(N)) 和 sum([x2 for x in range(N)]) 哪个快?大多数人觉得生成器快,实际不一定。生成器省内存是肯定的,但每次 yield 有函数调用开销。列表推导式的循环在 C 层执行(CPython 实现中 listcomp 是专用字节码),而生成器每次 yield 要切换栈帧。数据量小时列表版反而更快——省下的内存分配开销比 yield 开销小。数据量大时生成器版才赢,因为列表版要先把所有结果存内存。经验值:N 10 万生成器才有明显优势。字典推导式和集合推导式呢?语法一样,换括号就行:{k: v for k, v in pairs} 是字典推导式,{x for x in items} 是集合推导式。注意集合推导式没有生成器版本——{x for x in items} 是立即求值的,没有惰性集合。如果需要惰性去重,得用生成器 + set() 分两步。嵌套推导式怎么读?从左到右读,和嵌套 for 循环的顺序一致。[f(x,y) for x in A for y in B] 等价于 for x in A: for y in B: f(x,y)。超过两层就该换普通循环了,没人能在脑内解析三层推导式。Python 之禅说得明白:可读性很重要。写段代码# 生成器只能遍历一次的坑gen = (x**2 for x in range(5))print(list(gen)) # [0, 1, 4, 9, 16]print(list(gen)) # [] ← 耗尽了!# 需要多次使用就转列表squares = [x**2 for x in range(5)]print(squares[:3]) # [0, 1, 4]print(squares[3:]) # [9, 16]# 管道式处理用生成器省内存lines = (line.strip() for line in open('big.log')) # 不读全文errors = (line for line in lines if 'ERROR' in line)count = sum(1 for _ in errors) # 只计数,不存结果
服务端阅读 05月29日 00:05

Python 面向对象的核心概念有哪些?MRO 和描述符怎么理解?

Python 面向对象的核心是四件事:用类组织数据和行为的封装机制、通过继承复用代码、用多态让不同对象响应同一接口、以及 Python 自己的特殊之处——MRO、描述符、slots 这些面试高频考点。基础概念(类/实例/属性/方法)不展开,下面只说容易踩坑和被追问的部分。追问Python 的 MRO 是怎么排的?为什么不用深度优先?Python 3 用 C3 线性化算法计算 MRO。核心规则:子类排在父类前面,同一层按定义顺序排,不能违反前两条规定。为什么不用深度优先?因为菱形继承下深度优先会重复访问基类。经典例子:D 继承 B 和 C,B 和 C 都继承 A,深度优先的顺序是 D→B→A→C→A,A 被访问两次。C3 的结果是 D→B→C→A,每个类只出现一次,且 B 在 C 前面(定义顺序)。通过 ClassName.__mro__ 可以查看任意类的解析顺序。slots 能省多少内存?有什么代价?普通 Python 对象用 __dict__ 存属性,一个空对象就要占 56 字节(64 位 CPython)。__slots__ 用固定数组替代字典,属性直接按偏移量访问,省掉哈希表开销。实际测量:100 万个只有 name 和 age 属性的对象,用 __dict__ 约 160MB,用 __slots__ 约 48MB,省 70%。代价是不能再动态添加属性,而且继承时如果父类没有声明 __slots__,子类照样会有 __dict__,优化白做。实际项目中,Django 的 QuerySet 用了 __slots__ 优化大量小对象。描述符是什么?property 和 classmethod 跟它什么关系?描述符是实现了 __get__、__set__、__delete__ 中任意一个的类。Python 的属性查找有个隐藏步骤:如果找到的对象是描述符,就调用它的 __get__ 返回结果,而不是直接返回对象本身。property 就是描述符——你的 getter/setter 被 __get__/__set__ 包装了;classmethod 也是描述符——它的 __get__ 把类传给函数而不是实例。区分数据描述符(有 __set__)和非数据描述符(只有 __get__):数据描述符优先级高于实例 __dict__,非数据描述符优先级低于实例 __dict__。这就是为什么 property 能拦截赋值而普通方法不行。new 和 init 有什么区别?__new__ 创建对象并返回,__init__ 初始化已创建的对象。__new__ 是类方法(第一个参数是 cls),__init__ 是实例方法(第一个参数是 self)。单例模式用 __new__ 控制:如果 _instance 已存在就直接返回,不再创建新对象。__init__ 做不到这点——它执行时对象已经创建了。另一个场景:不可变类型(str、int、tuple)的子类化必须重写 __new__,因为这些类型的对象在 __new__ 阶段就已经确定了值,__init__ 改不了。写段代码# 描述符实现懒加载属性class LazyProperty: def __init__(self, func): self.func = func def __get__(self, obj, cls): if obj is None: return self value = self.func(obj) obj.__dict__[self.func.__name__] = value # 缓存到实例字典 return valueclass Data: @LazyProperty def expensive(self): print("计算中...") return sum(range(1000000))d = Data()print(d.expensive) # 计算中... 499999500000print(d.expensive) # 499999500000(不再计算,从 __dict__ 直接取)
服务端阅读 05月28日 09:38

Redis 安全配置怎么做?生产环境加固 checklist 与漏洞防范

核心回答Redis 安全配置要从网络隔离、认证授权、数据保护和运行加固四个层面入手。生产环境最低要求三条:绑定内网 IP + 开启密码认证 + 禁用危险命令。做不到这三条,Redis 基本等于裸奔。2015 年爆发的 Redis 未授权访问漏洞(CVE-2015-4335)让大量服务器被植入挖矿脚本和 SSH 公钥,根源就是默认配置下 Redis 无密码监听所有网卡。这个漏洞至今仍在被批量扫描利用,绝不是历史问题。每次安全加固的第一步,就是确保这三条底线全部到位。网络层隔离绑定监听地址Redis 默认绑定 0.0.0.0,所有网卡都能连,这是最常见的安全隐患。修改 redis.conf:bind 127.0.0.1 10.0.0.1protected-mode yesprotected-mode 是 Redis 3.2 引入的保护机制,当 Redis 绑定了非回环地址且没有设置密码时,会拒绝外部连接。这个开关一定要保持开启。生产环境建议只绑定内网 IP,绝不要直接暴露到公网。如果必须远程访问,走 VPN 或 SSH 隧道。很多 Redis 被入侵的案例就是因为公网暴露了 6379 端口,被扫描器批量发现后利用。防火墙规则即使绑定了内网 IP,也要用防火墙做二次防护,这是纵深防御的基本思路:# iptables 方式iptables -A INPUT -p tcp --dport 6379 -s 10.0.0.0/24 -j ACCEPTiptables -A INPUT -p tcp --dport 6379 -j DROP# firewalld 方式firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port protocol="tcp" port="6379" accept'firewall-cmd --reload云上环境用安全组实现同样的效果,原则是端口最少放开。AWS、阿里云的安全组规则中,Redis 端口只对应用服务器所在子网开放。TLS 加密传输Redis 6.0 开始原生支持 TLS,这是生产环境安全加固的重要一环。如果数据经过不可信网络(跨机房、公网),必须开启:# 生成证书openssl genrsa -out redis.key 2048openssl req -new -key redis.key -out redis.csropenssl x509 -req -days 365 -in redis.csr -signkey redis.key -out redis.crt# redis.conf 配置tls-port 6380port 0tls-cert-file /path/to/redis.crttls-key-file /path/to/redis.keytls-ca-cert-file /path/to/ca.crt设置 port 0 关闭明文端口,强制所有连接走 TLS。集群模式下还需要配置 tls-cluster yes 和 tls-replication yes。Redis 7.0 进一步增强了 TLS 支持,集群总线通信也可以走加密通道。认证与授权密码认证最基本的安全措施,也是 Redis 安全加固 checklist 的第一条:# redis.confrequirepass your_strong_password# 运行时设置(重启失效)CONFIG SET requirepass your_strong_password# 连接时指定密码redis-cli -a your_strong_password注意:-a 参数会触发警告,密码可能出现在进程列表和日志中。建议用 REDISCLI_AUTH 环境变量代替:export REDISCLI_AUTH=your_strong_passwordredis-cli密码强度要求:至少 16 位,混合大小写字母、数字和特殊字符。requirepass 的密码以明文存储在配置文件中,所以配置文件权限也要收紧。ACL 精细权限控制Redis 6.0 引入 ACL(Access Control List),替代了之前只有一个全局密码的模式,是实现 Redis 安全最小权限原则的关键:# 创建只读用户,只能访问 user: 开头的 keyACL SETUSER readonly on >password1 ~user:* +@read# 创建业务用户,只能操作特定前缀的 keyACL SETUSER app_user on >app_password ~order:* +@read +@write +@string +@hash# 创建管理员ACL SETUSER admin on >admin_password ~* +@all# 查看所有用户ACL LIST# 删除用户ACL DELUSER readonlyACL 的权限粒度可以精确到命令组和 key 模式。+@read 表示所有读命令,+@write 表示所有写命令,+@all 表示全部命令。用 ACL CAT 查看所有命令组。实际部署中,为每个业务应用创建独立的 ACL 用户,遵循最小权限原则。一个只做缓存的业务不需要 FLUSHALL 权限。Redis 7.0 还支持 ACL 规则持久化到文件,通过 aclfile 配置项指定,比每次重启都重新配置更可靠。禁用和重命名危险命令这是防止 Redis 被攻击者利用的关键配置:# redis.confrename-command FLUSHALL ""rename-command FLUSHDB ""rename-command CONFIG ""rename-command SHUTDOWN ""rename-command DEBUG ""# 或者重命名为难猜的名字rename-command FLUSHALL "a9b8c7d6e5_FLUSHALL"禁用比重命名更安全。如果用重命名,新的命令名不要出现在代码和日志中。CONFIG 命令尤其危险,攻击者可以通过它修改 requirepass 实现持久化后门——先把密码改成自己知道的值,再修改 dir 指向 /root/.ssh/,dbfilename 设为 authorized_keys,执行 BGSAVE 写入 SSH 公钥。这就是 CVE-2015-4335 的经典攻击链。数据安全持久化策略# RDB 快照save 900 1save 300 10save 60 10000# AOF 日志appendonly yesappendfsync everysecRDB 适合做备份,AOF 适合做数据安全。生产环境建议两者都开,AOF 保证最多丢 1 秒数据,RDB 做快速恢复的兜底。注意:AOF 文件可能包含敏感数据(如密码明文),需要控制文件访问权限。加密持久化文件Redis 本身不提供数据加密,需要在文件系统层面解决:# 限制文件权限chmod 700 /var/lib/redischmod 600 /var/lib/redis/dump.rdbchmod 600 /var/lib/redis/appendonly.aof# 文件系统加密(Linux)# 使用 LUKS 或 eCryptfs 加密 Redis 数据目录如果数据敏感性高(如用户信息、Token),在写入 Redis 前做应用层加密。读取时解密,Redis 只存密文。这样即使持久化文件被窃取,也无法直接获取明文数据。备份与恢复# 定时备份 RDB0 2 * * * cp /var/lib/redis/dump.rdb /backup/dump_$(date +\%Y\%m\%d).rdb# 备份到远程rsync -avz /backup/ user@remote:/backup/备份文件也要控制权限,最好加密后传输。恢复时注意检查 RDB 文件完整性,避免被篡改的备份文件引入恶意数据。用 redis-check-rdb 工具校验 RDB 文件,用 redis-check-aof 校验 AOF 文件。运行时加固最小权限运行# 创建专用用户useradd -r -s /bin/false redis# 设置文件归属chown -R redis:redis /var/lib/redischown redis:redis /etc/redis/redis.conf# 用非 root 用户启动sudo -u redis redis-server /etc/redis/redis.confRedis 不需要 root 权限。用 root 运行 Redis 一旦被攻破,攻击者可以直接拿到服务器控制权,这就是为什么 Redis 安全加固必须包含权限降级。文件权限收紧chmod 600 /etc/redis/redis.confchmod 700 /var/lib/redischmod 600 /var/log/redis/redis.log配置文件包含密码等敏感信息,必须限制读写权限。日志文件可能包含查询内容,也要保护。系统级隔离用 systemd 的安全选项加强隔离:[Service]User=redisGroup=redisExecStart=/usr/bin/redis-server /etc/redis/redis.confProtectSystem=fullReadWritePaths=/var/lib/redisNoNewPrivileges=truePrivateTmp=trueNoNewPrivileges=true 防止子进程提权,PrivateTmp=true 隔离临时目录。Docker 部署时同理,不要用 --privileged 参数,用 --user 指定非 root 用户,用 --cap-drop=ALL 去掉不必要的 Linux capabilities。监控与审计慢查询监控# redis.confslowlog-log-slower-than 10000slowlog-max-len 128# 查看慢查询SLOWLOG GET 10慢查询日志可以帮助发现异常操作。突然出现的慢查询可能是攻击者在执行大量 KEYS * 扫描或 DEL 删除。Prometheus + Grafana 监控# prometheus.ymlscrape_configs: - job_name: 'redis' static_configs: - targets: ['localhost:9121']# 告警规则groups: - name: redis_alerts rules: - alert: RedisTooManyConnections expr: redis_connected_clients > 100 for: 1m labels: severity: warning - alert: RedisSuspiciousCommands expr: rate(redis_commands_processed_total[5m]) > 10000 for: 2m labels: severity: critical关键监控指标:连接数突增、内存使用异常、命令执行频率异常、主从切换。连接数异常暴增可能是在做端口扫描或暴力破解密码,命令频率异常可能是攻击者在批量导出数据。操作审计Redis 本身的审计能力有限,建议在以下层面补充:网络层:记录所有连接来源 IP,排查可疑来源应用层:在业务代码中记录关键操作,特别是写入和删除操作系统层:用 auditd 监控 redis.conf 文件变更,防止配置被篡改如果合规要求严格,可以考虑 Redis 企业版的审计日志功能,或用第三方审计代理。集群安全主从复制认证# 主节点配置masterauth your_master_passwordrequirepass your_master_password# 从节点配置requirepass your_slave_passwordmasterauth your_master_password主从之间必须设置认证。没有认证的复制关系,攻击者可以伪装成从节点拉取全量数据,这相当于直接把数据库内容交给了攻击者。哨兵模式安全# sentinel.confsentinel auth-pass mymaster your_master_password哨兵也需要配置认证密码,否则攻击者可以操控哨兵触发故障转移,把主节点切换到自己控制的服务器,实现中间人攻击。集群模式安全Redis 7.0 开始支持集群 TLS,集群总线通信也走加密通道:cluster-enabled yescluster-config-file nodes.confcluster-node-timeout 5000tls-cluster yestls-replication yes集群模式下的安全比单机更复杂,因为节点间通信也需要保护。Redis 7.2+ 还支持集群总线端口的 TLS 认证,确保集群内部通信不被窃听。安全加固优先级按紧迫程度排序,这份 Redis 安全加固 checklist 可以直接参考:立即做:绑定内网 IP + 开启密码认证 + 禁用危险命令。不做这三条就是在等被入侵尽快做:配置防火墙 + 使用非 root 用户 + 收紧文件权限。降低被攻破后的影响范围逐步做:开启 TLS + 配置 ACL + 部署监控告警。提升整体安全水位持续做:更新版本修复 CVE + 审计日志 + 定期安全扫描。保持安全性不退化已知安全漏洞Redis 历史上几个重要的安全漏洞,面试和实战都会遇到:CVE-2015-4335:未授权访问写入 SSH 公钥和 cron 任务,这是最经典也最常被利用的 Redis 安全漏洞。攻击条件极其简单:Redis 暴露公网 + 无密码CVE-2022-0543:Debian/Ubuntu 打包的 Lua 沙箱逃逸,可以在 Redis 中执行任意代码。影响范围广,因为大部分 Linux 发行版都用系统包管理器安装 RedisCVE-2023-41053:Lua 脚本库堆栈溢出,可导致拒绝服务CVE-2025-32023:Redis 7.4.x 之前的 Lua 脚本 eval 逃逸漏洞,最新一轮安全修复面试中被问到 Redis 安全,提到 CVE-2015-4335 说明你理解问题的根源:默认配置不安全。提到 ACL 说明你跟进 Redis 6.0+ 新版本特性。提到加固优先级说明你有生产环境实战经验。应急响应发现 Redis 被入侵时的处理步骤:立即断网:iptables -A INPUT -p tcp --dport 6379 -j DROP,先止血保留现场:SLOWLOG GET 100、CLIENT LIST、INFO 记录当前状态,为后续分析保留证据检查后门:查 crontab、SSH authorized_keys、.bashrc 是否被篡改,这是 Redis 被入侵后最常见的持久化手段分析入侵路径:检查 CONFIG GET dir 和 CONFIG GET dbfilename 是否被改过,确认数据文件是否被指向了系统敏感目录清除恢复:在确保后门清除后,修改密码重启 Redis,从备份恢复数据加固复盘:按上面的安全加固 checklist 逐项检查,堵住入侵路径大多数 Redis 被入侵事件的根因都是:公网暴露 + 无密码 + CONFIG 命令可用。攻击者通过 CONFIG SET dir /root/.ssh/ CONFIG SET dbfilename authorized_keys 写入 SSH 公钥实现持久化控制。理解这个攻击链,就知道为什么禁用 CONFIG 命令是 Redis 安全加固的核心措施。
服务端阅读 05月28日 09:37

Redis缓存穿透、击穿、雪崩有什么区别?

Redis 缓存策略是使用 Redis 作为缓存时的核心问题,需要解决缓存穿透、缓存击穿、缓存雪崩等问题,同时需要设计合理的缓存更新策略。缓存穿透:查不存在的数据怎么办?缓存穿透是指查询一个数据库和缓存中都不存在的数据,请求每次都会穿过缓存直接打到数据库。典型场景:攻击者用不存在的 ID 批量请求接口,如 /user/-1。方案一:缓存空对象当数据库也查不到时,将空值写入缓存并设置短 TTL,避免同一 key 反复穿透。public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return "NULL".equals(user) ? null : user; } user = db.queryUserById(id); if (user == null) { redis.set("user:" + id, "NULL", 300); // 缓存空对象,5分钟过期 } else { redis.set("user:" + id, user, 3600); } return user;}注意:空对象 TTL 不宜过长,否则该 key 对应的真实数据写入后,缓存仍是空值,导致数据不一致。可结合主动删除策略,写入数据时同步删除空缓存。方案二:布隆过滤器在缓存前加一层布隆过滤器,快速判断 key 是否可能存在。布隆过滤器说不存在则一定不存在,说存在则可能存在(有误判率)。if (!bloomFilter.mightContain("user:" + id)) { return null; // 一定不存在,直接返回}User user = redis.get("user:" + id);if (user != null) { return user;}user = db.queryUserById(id);if (user != null) { redis.set("user:" + id, user, 3600);}return user;选型建议:数据量小且查询模式固定 → 缓存空对象更简单;数据量大且 key 空间稀疏 → 布隆过滤器更省内存。缓存击穿:热点 key 过期瞬间怎么办?缓存击穿是指某个热点 key 在过期的一瞬间,大量并发请求同时查询该 key,全部穿透到数据库。与雪崩的区别:击穿是单个热点 key,雪崩是大面积 key 同时失效。方案一:互斥锁用分布式锁保证只有一个线程查库并回填缓存,其他线程等待后重试。public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return user; } String lockKey = "lock:user:" + id; try { if (redis.setnx(lockKey, "1", 10)) { // 获取锁,10秒自动释放 user = db.queryUserById(id); redis.set("user:" + id, user, 3600); } else { Thread.sleep(100); return getUserById(id); // 等待后重试 } } finally { redis.del(lockKey); } return user;}注意:锁要设超时时间防止死锁;重试要设上限防止无限递归。方案二:逻辑过期缓存永不过期,在值中存一个逻辑过期时间戳。读到逻辑过期数据时,异步更新缓存,当前请求返回旧数据。牺牲短暂一致性换取高可用。public User getUserById(Long id) { String value = redis.get("user:" + id); if (value != null) { JSONObject json = JSON.parseObject(value); if (json.getBoolean("expired")) { asyncUpdateCache(id); // 异步更新,不阻塞当前请求 } return json.getObject("data", User.class); // 返回旧数据 } User user = db.queryUserById(id); JSONObject json = new JSONObject(); json.put("data", user); json.put("expired", false); redis.set("user:" + id, json.toJSONString(), 3600); return user;}选型建议:一致性要求高 → 互斥锁;可用性要求高、允许短暂脏读 → 逻辑过期。缓存雪崩:大面积 key 同时失效怎么办?缓存雪崩是指大量 key 在同一时间过期,或 Redis 宕机,导致请求全部打到数据库。方案一:过期时间加随机偏移给 TTL 加上随机值,避免大量 key 在同一时刻集中过期。int expire = 3600 + new Random().nextInt(600); // 3600~4200秒随机redis.set("user:" + id, user, expire);方案二:缓存预热系统启动或低峰期提前加载热点数据到缓存,避免冷启动时流量直接打库。@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点预热public void warmUpCache() { List<User> users = db.queryHotUsers(); for (User user : users) { int expire = 3600 + new Random().nextInt(600); redis.set("user:" + user.getId(), user, expire); }}方案三:高可用 + 降级部署 Redis Sentinel 或 Cluster,避免单点故障数据库前加熔断限流,雪崩时快速失败而非拖垮数据库使用本地缓存(Caffeine/Guava)作为二级缓存兜底public User getUserById(Long id) { try { User user = redis.get("user:" + id); if (user != null) { return user; } } catch (Exception e) { log.error("Redis error", e); return localCache.get("user:" + id); // 降级到本地缓存 } User user = db.queryUserById(id); redis.set("user:" + id, user, 3600); return user;}缓存更新策略怎么选?三种常见策略,适用场景不同:Cache Aside(旁路缓存):读时回填,写时删缓存。最常用,一致性较好。// 读User user = redis.get("user:" + id);if (user == null) { user = db.queryUserById(id); redis.set("user:" + id, user, 3600);}// 写db.updateUser(user);redis.del("user:" + user.getId()); // 删缓存而非更新缓存为什么删缓存而不是更新?并发写时更新缓存可能出现旧值覆盖新值的问题,删除更安全,下次读时自然回填最新值。Write Through(写穿透):写数据时同步更新缓存和数据库。数据一致性强,但写延迟高,适合写少读多的场景。Write Behind(写回):先更新缓存,异步批量写入数据库。写性能极高,但有数据丢失风险,适合写密集且容忍少量丢失的场景(如浏览量计数)。缓存和数据库不一致怎么处理?缓存与数据库不一致是分布式系统的经典问题,根本原因是两者无法原子操作。方案一:延时双删先删缓存 → 更新数据库 → 延时再删缓存。第二次删除用于清除更新数据库期间被旧缓存回填的数据。public void updateUser(User user) { redis.del("user:" + user.getId()); // 第一次删除 db.updateUser(user); Thread.sleep(500); // 延时,确保读请求回填旧缓存完成 redis.del("user:" + user.getId()); // 第二次删除}缺点:延时时间难以精确设定,过长影响性能,过短仍可能不一致。方案二:订阅 Binlog通过 Canal 等中间件订阅数据库 Binlog,数据变更时自动更新或删除缓存。解耦业务代码,一致性更可靠。@CanalEventListenerpublic class CacheUpdateListener { @ListenPoint(destination = "example", schema = "test", table = "user") public void onEvent(CanalEntry.Entry entry) { User user = parseUserFromBinlog(entry); redis.del("user:" + user.getId()); // 或更新缓存 }}选型建议:一致性要求一般 → 延时双删够用;一致性要求高 → Binlog 方案更可靠。面试怎么答?被问到这三个概念时,建议按以下结构回答:先说定义:穿透是数据不存在,击穿是热点 key 过期,雪崩是大面积 key 同时失效再说区别:穿透查的是不存在的数据,击穿和雪崩查的是存在的数据;击穿是单个 key,雪崩是批量 key最后说方案:穿透用空缓存或布隆过滤,击穿用互斥锁或逻辑过期,雪崩用随机过期+预热+高可用追问方向:生产环境怎么监控缓存健康度?关注缓存命中率(低于 80% 需告警)、慢查询日志、内存使用率和 key 过期分布。
服务端阅读 05月28日 09:37

Redis 如何进行监控和运维?

Redis 监控和运维是保障线上稳定性的核心能力,面试中常从"监控哪些指标""用什么工具""遇到问题怎么排查"三个角度考察。关键监控指标内存指标内存是 Redis 最核心的资源,重点监控以下项:INFO memory# 关键字段used_memory # Redis 分配器分配的内存used_memory_rss # 操作系统实际分配的内存mem_fragmentation_ratio # 内存碎片率 = used_memory_rss / used_memorymaxmemory # 配置的最大内存限制碎片率阈值解读:碎片率 > 1.5 说明碎片严重,需触发 MEMORY PURGE 或重启;碎片率 < 1.0 说明使用了 Swap,性能会急剧下降,应立即排查。命中率直接反映缓存有效性:keyspace_hits / (keyspace_hits + keyspace_misses)命中率低于 90% 时需排查是否有过期 key 未清理、缓存穿透等问题。性能与延迟指标INFO stats# instantaneous_ops_per_sec — 当前 QPS# total_commands_processed — 累计处理命令数# 延迟监控(Redis 2.8.13+)CONFIG SET latency-monitor-threshold 100 # 超过 100ms 记录LATENCY LATEST # 查看最近延迟事件LATENCY DOCTOR # 诊断延迟原因QPS 突降或延迟突增是故障的前兆,应设置基线告警。连接与复制指标INFO clients# connected_clients — 当前连接数# blocked_clients — 阻塞等待的客户端数INFO replication# master_repl_offset / slave_repl_offset — 主从复制偏移量差即同步延迟# master_link_down_since_seconds — 主从断连时长,应为 0连接数超过 maxclients 的 80% 就应告警;主从偏移量差持续增大说明同步瓶颈。持久化指标INFO persistence# rdb_last_save_time — 最后 RDB 保存时间# rdb_changes_since_last_save — 上次保存后变更数,过大说明 RDB 间隔过长# aof_rewrite_in_progress — AOF 重写是否进行中# aof_current_size / aof_base_size — AOF 文件大小,重写触发比默认 100%监控工具选型原生命令INFO:全局状态快照,按 section 查看内存、客户端、复制等SLOWLOG:慢查询日志,SLOWLOG GET 10 查看最近 10 条,关注 usec 字段MONITOR:实时命令流,生产环境慎用(会降低吞吐约 50%),仅用于紧急排查LATENCY:延迟监控框架,可记录延迟事件并给出诊断建议可观测性体系Prometheus + Redis Exporter + Grafana 是生产环境主流方案:# 部署 Redis Exporterdocker run -d --name redis-exporter \ -e REDIS_ADDR=redis://localhost:6379 \ prom/redis-exporter# Prometheus 抓取配置scrape_configs: - job_name: redis static_configs: - targets: ["localhost:9121"]Grafana 可导入 Redis Dashboard(ID: 11835),覆盖内存、QPS、命中率、延迟等核心面板。Redis Insight 是 Redis 官方可视化工具,支持内存分析、CLI、Profiler,适合开发调试阶段。哨兵监控使用 Sentinel 时,除上述指标外还需关注:SENTINEL masters # 主节点状态SENTINEL slaves <master> # 从节点状态SENTINEL sentinels <master> # 哨兵节点状态重点监控主观下线/客观下线事件、故障转移耗时。常见故障排查内存不足现象:OOM command not allowed when used memory > maxmemoryINFO memory # 查看内存使用redis-cli --bigkeys # 扫描大 KeyMEMORY USAGE <key> # 查看单个 key 内存占用解决:调整 maxmemory、设置淘汰策略 allkeys-lru、用 UNLINK 替代 DEL 异步删除大 Key(避免阻塞主线程)、分批 SCAN 删除。慢查询SLOWLOG GET 10 # 查看慢查询CONFIG GET slowlog-log-slower-than # 查看阈值常见原因:KEYS * 全量扫描、大 Key 操作(HGETALL 千万级 hash)、SORT 命令。替代方案:SCAN 替代 KEYS,HSCAN 替代 HGETALL,Pipeline 减少网络往返。主从同步延迟INFO replication# master_repl_offset 与 slave_repl_offset 的差值增大 repl-backlog-size、优化网络、避免主节点大 Key 写入导致积压。连接数打满INFO clientsCONFIG GET maxclients设置 timeout 自动断开空闲连接、排查连接泄漏、适当调大 maxclients。运维操作要点备份恢复RDB 备份用 BGSAVE(后台执行,不阻塞主线程),AOF 备份直接拷贝 .aof 文件。恢复时停止 Redis → 替换文件 → 启动。生产建议 RDB + AOF 混合持久化(Redis 4.0+)。数据迁移MIGRATE 命令:迁移单个或多个 key,原子操作RedisShake:阿里开源工具,支持全量+增量同步,适合大规模迁移# RedisShake 配置source.type: standalonesource.address: source.redis.com:6379target.type: standalonetarget.address: target.redis.com:6379./redis-shake.linux -type=sync -conf=shake.conf集群扩缩容# 添加节点redis-cli --cluster add-node <new-ip>:<port> <exist-ip>:<port># 重新分配槽位redis-cli --cluster reshard <exist-ip>:<port> \ --cluster-from <src-node-id> \ --cluster-to <dst-node-id> \ --cluster-slots 1000删除节点前必须先迁走其槽位,否则拒绝删除。性能优化配置# 内存maxmemory 2gbmaxmemory-policy allkeys-lruecho never > /sys/kernel/mm/transparent_hugepage/enabled # 关闭 THP# 持久化appendonly yesappendfsync everysec # 折中方案,最多丢 1 秒数据auto-aof-rewrite-min-size 64mb# 网络tcp-backlog 511tcp-keepalive 300timeout 300 # 空闲连接超时告警建议:内存使用率 > 80%、QPS 下降 > 50%、延迟 > 100ms、连接数 > 80% maxclients。以上内容覆盖了 Redis 监控运维的核心考察点,面试中回答时应遵循"指标→工具→排查→优化"的递进逻辑,展示系统性思维而非零散知识点。
服务端阅读 05月28日 09:36

Redis 的过期策略和内存淘汰机制是什么?如何选择合适的策略?

Redis 的过期策略和内存淘汰机制是两个不同层面的问题:过期策略决定「过期 key 何时被删」,内存淘汰策略决定「内存不够时删谁」。过期策略Redis 采用惰性删除 + 定期删除的组合策略,不使用定时删除。惰性删除:访问 key 时才检查是否过期。由 expireIfNeeded() 函数实现,所有读写命令执行前都会调用。优点是 CPU 友好,缺点是过期 key 若不被访问就永远占内存。定期删除:每秒执行约 10 次(受 hz 配置控制),每次随机抽取 20 个设置了过期时间的 key 检查,若过期则删除。若本轮过期 key 超过 25%,则继续抽样,直到低于 25% 或超时(25ms)。由 activeExpireCycle() 函数实现。为什么不单独用定时删除?创建大量定时器会严重消耗 CPU 资源,Redis 出于性能考虑弃用此方案。二者配合的效果:定期删除保证过期 key 不会长期滞留,惰性删除兜底处理定期删除遗漏的 key。内存淘汰策略当 Redis 内存使用达到 maxmemory 限制时,根据淘汰策略决定删除哪些 key。共 8 种策略:| 策略 | 淘汰范围 | 算法 | 适用场景 ||------|---------|------|----------|| noeviction | 不淘汰 | - | 数据不能丢失 || allkeys-lru | 全部 key | LRU | 纯缓存,热点集中 || allkeys-lfu | 全部 key | LFU | 纯缓存,访问频率差异大 || allkeys-random | 全部 key | 随机 | 所有 key 访问概率相近 || volatile-lru | 有过期时间的 key | LRU | 混合存储,保留持久数据 || volatile-lfu | 有过期时间的 key | LFU | 同上,优先淘汰低频临时数据 || volatile-random | 有过期时间的 key | 随机 | 临时数据随机淘汰 || volatile-ttl | 有过期时间的 key | TTL 最短 | 优先淘汰即将过期的 key |LRU 与 LFU 的实现Redis 使用近似 LRU,并非精确 LRU。每个 key 记录最后访问时间戳(24bit lru 字段),淘汰时随机采样 N 个 key(默认 5 个),删除其中最久未访问的。采样数越大越接近精确 LRU,但 CPU 开销也越大。LFU 在 Redis 4.0 引入,复用 lru 字段的高 16 位记录衰减时间、低 8 位记录访问计数器。计数器会随时间衰减,避免历史高频 key 永远不被淘汰。如何选择纯缓存场景(数据全可丢失):allkeys-lru(推荐)或 allkeys-lfu部分数据持久化(重要数据不设过期时间):volatile-lru 或 volatile-lfu数据绝对不能丢:noeviction所有 key 访问概率均匀:allkeys-random生产环境推荐优先考虑 allkeys-lru,绝大多数缓存场景都适用。若使用 Redis 4.0+ 且访问频率差异明显,allkeys-lfu 更精准。用 INFO memory 命令监控内存使用,关注 used_memory、used_memory_peak、maxmemory 等指标。追问Q:过期策略和内存淘汰策略的关系?过期策略处理的是「已过期的 key 何时删除」,是时间驱动的;内存淘汰策略处理的是「内存不足时删谁」,是空间驱动的。两者互补:即使过期策略遗漏了部分过期 key,内存淘汰策略也能在内存紧张时兜底清理。Q:为什么 Redis 的 LRU 是近似的?精确 LRU 需要维护全局链表,每次访问都要移动节点,O(1) 的 key 访问变成 O(n) 的链表操作。近似 LRU 随机采样 5 个 key 淘汰最旧的,牺牲少量精度换取 O(1) 的访问性能。实测表明近似 LRU 的命中率接近精确 LRU。Q:volatile 系列策略的一个潜在问题?如果没有 key 设置过期时间,volatile 策略等同于 noeviction,不会淘汰任何 key,可能导致内存满后写入全部失败。