mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3243 字
9 分钟
Redis为何高性能?
2026-04-29

Redis实现验证码登录及签到功能代码解析#

📋 整体概述#

该类主要负责用户登录、发送验证码、签到等核心业务功能,采用 Redis + 手机号验证码 的无密码登录方案,适配分布式项目部署需求。

🔍 Session 与 Redis 实现登录系统的对比#

本项目采用 Redis + Token 方案实现登录认证,相较于传统 Session 方案,在分布式场景、多端适配、性能稳定性上具有显著优势,具体对比如下:

一、核心痛点解决:分布式集群会话共享问题#

  • 业务场景:单体项目时代,单台服务器即可承载所有请求,Session 方案简单便捷;但电商项目流量激增后,需部署多台服务器并通过 Nginx 负载均衡,保障系统高可用。

  • Session 的劣势:Session 存储在单台服务器(如 Tomcat)的内存中,属于“服务器绑定”。若用户在 A 服务器登录,下一次请求被 Nginx 转发至 B 服务器,B 服务器无该用户 Session,会判定用户未登录,严重影响用户体验。

  • Redis 的优势:将用户登录凭证(Token 关联的用户信息)统一存储在独立的 Redis 数据库中,所有业务服务器均为“无状态”。无论用户请求落在哪台服务器,都统一从 Redis 核对登录状态,完美解决会话共享问题,适配分布式集群部署。

二、业务拓展:多端支持与跨域兼容#

  • 业务场景:成熟产品通常同时拥有 PC 端网页、微信小程序、原生 App 等多端载体,需一套后端接口适配所有终端。

  • Session 的劣势:Session 机制强依赖浏览器 Cookie,通过 Cookie 传递 JSESSIONID 维持会话。但 App、小程序默认无 Cookie 机制,前端处理繁琐;且前后端分离架构中,Cookie 易受跨域限制,适配成本高。

  • Redis 的优势:Redis + Token 方案彻底解耦前后端。用户登录后,后端仅返回 Token 字符串,前端将其本地存储(如 localStorage、App 本地缓存),后续请求只需将 Token 放入 HTTP 请求头即可。该逻辑适用于 Web、App、小程序所有终端,实现“一套接口,多端复用”。

三、性能与稳定性:解放业务服务器内存压力#

  • 业务场景:大促、秒杀等场景下,同时在线人数可达数十万、上百万,对系统并发能力和稳定性要求极高。

  • Session 的劣势:用户登录态(Session)存储在业务服务器 JVM 内存中,数十万用户会导致 JVM 内存占用激增,引发频繁垃圾回收(GC),造成系统卡顿,严重时会出现 OOM(内存溢出),导致服务宕机。

  • Redis 的优势:Redis 是专业内存数据库,性能极强(单机能支撑 10万+ QPS),专门用于存储键值对数据。将用户登录态存储交给 Redis,可彻底解放业务服务器 JVM 内存,让业务服务器专注于逻辑计算和业务处理,提升并发能力;同时支持动态增加服务器节点,轻松应对流量峰值。

🔧 代码实现#

1️⃣ 发送验证码 - sendCode()#

**方法定义**

public Result<String> sendCode(String phone, HttpSession session){
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到Redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.调用第三方短信服务发送验证码
boolean sendSuccess = smsService.sendVerificationCode(phone, code);
if (!sendSuccess) {
log.error("短信发送失败,手机号:{}", phone);
return Result.fail("短信发送失败,请稍后重试");
}
// 6.发送成功
log.info("发送短信验证码成功,手机号:{}", phone);
// 返回ok(生产环境不应该返回验证码)
return Result.ok("验证码已发送");
}

**执行流程**

  1. 校验手机号格式(通过正则表达式判断,确保手机号合法);

  2. 生成6位随机数字验证码,作为用户登录验证凭证;

  3. 调用阿里服务,向用户发送短信;

  4. 将验证码存入Redis,缓存key格式为 `login:code:手机号`,有效期由常量 LOGIN_CODE_TTL 控制;

2️⃣ 登录功能 - login()⭐ 核心方法#

**方法定义**(添加事务注解,保证多表操作原子性)

@Transactional(rollbackFor = Exception.class)
public Result<String> login(LoginFormDTO loginForm, HttpSession session){
}

**完整业务流程**

步骤1-2:校验手机号格式#

String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}

通过工具类 RegexUtils 校验手机号格式,非法则直接返回失败结果。

步骤3:验证验证码#

String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("验证码错误");
}
  • 从Redis中获取之前缓存的验证码;

  • 对比用户输入的验证码与缓存中的验证码,不一致则返回失败。

步骤4-6:查询或创建用户(登录即注册)#

UserPhone userPhone = userPhoneService.lambdaQuery()
.eq(UserPhone::getPhone, phone).one();
User user = null;
if (userPhone == null) {
user = createUserWithPhone(phone); // 新用户自动注册
} else {
user = lambdaQuery().eq(User::getPhone, userPhone.getPhone()).one();
}
  • 先查询 tb_user_phone 表(手机号分表设计),判断该手机号是否已关联用户;

  • 若未关联(新用户),调用私有方法 createUserWithPhone() 自动创建用户;

  • 若已关联(老用户),查询 tb_user 表获取用户基本信息。

步骤7:生成Token并存入Redis#

// 7.1 生成UUID作为token(用户登录凭证,唯一标识)
String token = UUID.randomUUID().toString(true);
// 7.2 将User对象转为Map(过滤null值,所有值转String,避免序列化问题)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3 存储到Redis Hash结构(便于单独获取用户某字段,节省内存)
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4 设置过期时间(转换为秒避免Redisson递归问题)
stringRedisTemplate.expire(tokenKey,
TimeUnit.SECONDS.convert(LOGIN_USER_TTL, TimeUnit.MINUTES),
TimeUnit.SECONDS);

**关键点**

  • Token采用随机UUID生成,确保唯一性,作为用户登录后的身份凭证;

  • 用户信息以Hash结构存储在Redis中,缓存key格式为 `login:token:{uuid}`;

  • 设置过期时间,实现用户登录态自动登出,提升系统安全性。

**重点 : **User对象转为Map格式的好处与优点#

将User对象转为Map格式,核心是适配Redis Hash存储特性、优化数据存储与使用体验,具体优势如下:

  • 适配Redis Hash结构:Redis Hash适合存储对象的多个字段,Map格式可直接通过putAll\(\)方法批量存入,无需手动拆分字段,操作简洁高效,贴合用户登录态存储的需求。

  • 避免序列化问题:直接存储User对象需进行序列化(如Java默认序列化、JSON序列化),易出现序列化失败、数据体积过大等问题;转为Map(键值均为基础类型/字符串)可直接存储,无需额外序列化操作,降低异常风险。

  • 过滤冗余数据,节省内存:通过setIgnoreNullValue\(true\)可过滤对象中的null值,避免存储无效数据;同时可按需筛选核心字段(如用户ID、手机号),无需存储整个对象,减少Redis内存占用。

  • 统一数据格式,便于读取:通过setFieldValueEditor将所有字段值转为String类型,避免因字段类型不一致导致的存储/读取异常,后续从Redis获取时可直接解析,无需额外类型转换。

  • 支持单个字段高效操作:Redis Hash支持单独获取/修改某个字段(如更新用户等级),无需读取整个对象,减少网络传输量和Redis IO开销,提升系统性能。

步骤8:维护用户等级集合 &amp; 返回Token#

try {
maintainLevelSetMembership(user.getId());
} catch (Exception e) {
// 忽略异常,避免影响登录主流程
}
return Result.ok(token);
  • 调用私有方法 maintainLevelSetMembership(),将用户ID加入对应等级的Redis Set集合(用于秒杀场景的用户分级筛选);

  • 捕获异常并忽略,确保非核心功能失败不影响用户登录主流程;

  • 返回Token给前端,前端后续请求携带Token实现身份验证。

3️⃣ 签到功能 - sign()(第146-160行)#

**方法定义**

public Result<Void> sign()

**执行流程**

  1. 获取当前登录用户的ID(从ThreadLocal中获取,登录拦截器已存入);

  2. 获取当前日期,格式化为 `yyyyMM` 格式(用于按月存储签到记录);

  3. 构建Redis key:`sign:{userId}`,按用户、按月存储签到数据;

  4. 计算今天是本月的第几天(转换为0开始的索引,如3月5日对应索引4);

  5. 使用Redis的 SETBIT 命令,将对应索引位置的bit设为1,标记为已签到。

**示例**:若用户ID为123,3月5日签到,执行命令 `SETBIT sign:123:202603 4 1`。

4️⃣ 统计连续签到天数 - signCount()(第162-202行)#

**方法定义**

public Result<Integer> signCount()

**执行流程**

  1. 获取当前登录用户ID和当前日期;

  2. 构建与签到功能相同的Redis key(`sign:{userId}`);

  3. 使用Redis的 BITFIELD 命令,获取本月截止今天的所有签到记录(返回一个十进制数);

  4. 通过位运算计算连续签到天数,核心逻辑如下:

while (true) {
if ((num & 1) == 0) break; // 最后一位是0,说明当天未签到,中断循环
count++; // 最后一位是1,签到天数+1
num >>>= 1; // 右移一位,检查前一天的签到状态
}

**原理**

  • Redis Bitmap 中每个bit对应一天,1代表已签到,0代表未签到;

  • 从最低位(当天)开始向前遍历,遇到0则停止,统计连续为1的位数,即为连续签到天数。

5️⃣ 创建用户 - createUserWithPhone()(第204-230行)🔒 私有方法#

**方法定义**

private User createUserWithPhone(String phone)

**执行流程**(登录即注册核心逻辑)

  1. 创建User对象,通过SnowflakeIdGenerator生成分布式唯一ID,设置手机号和随机昵称(如user_13800138000);

  2. 将User对象保存到 tb_user 表(用户主表);

  3. 创建UserInfo对象,设置默认用户等级为1,关联当前用户ID;

  4. 将UserInfo对象保存到 tb_user_info 表(用户信息扩展表);

  5. 调用 maintainLevelSetMembership() 维护用户等级集合(捕获异常,失败不影响注册);

  6. 创建UserPhone对象,关联用户ID和手机号,保存到 tb_user_phone 表;

  7. 返回创建完成的User对象。

**事务保证**:由于外层login()方法添加了@Transactional注解,该方法中的所有数据库操作(多表插入)均处于同一事务中,确保原子性,要么全部成功,要么全部回滚。

6️⃣ 维护用户等级集合 - maintainLevelSetMembership()(第232-245行)🔒 私有方法#

**方法定义**

private void maintainLevelSetMembership(Long userId)

**作用**:根据用户等级,将用户ID添加到对应等级的Redis Set集合中,便于后续秒杀等场景快速筛选特定等级用户。

**执行流程**

  1. 通过userInfoService查询当前用户的等级信息;

  2. 判断用户等级是否有效(非空、非负数);

  3. 若等级有效,调用redisCache.addForSet()方法,将用户ID加入对应Redis Set集合;

  4. Redis Set的key格式为:`seckill:user:level:{level}`(如level=1对应key为seckill:user:level:1)。

🎨 设计亮点#

✅ 核心优点#

  1. **登录即注册**:新用户首次登录无需单独注册,自动完成账号创建,降低用户使用门槛,提升用户体验;

  2. **无状态认证**:基于Token + Redis实现登录态管理,服务端不存储会话信息,支持分布式集群部署,解决Session共享问题;

  3. **分表设计**:将用户手机号单独存储在tb_user_phone表,便于后续水平分表,应对用户量激增场景;

  4. **高性能签到**:采用Redis Bitmap存储签到数据,极大节省内存(1个月仅需1字节存储一个用户的签到记录),且操作高效;

  5. **容错处理**:非核心功能(如用户等级集合维护)失败时捕获异常并忽略,不影响登录、注册等主业务流程,提升系统稳定性。

⚠️ 注意事项#

  1. **并发问题**:登录流程中未对同一手机号加锁,高并发场景下可能出现同一手机号重复创建用户的问题,建议添加分布式锁控制;

  2. **Session冗余**:login()和sendCode()方法参数中包含HttpSession,但实际未使用,完全依赖Redis实现登录态,可删除该参数减少冗余;

  3. **Token续期**:当前未实现Token自动续期机制,用户长时间不操作会导致Token过期、被迫重新登录,生产环境建议添加Token续期逻辑(如拦截器中刷新过期时间)。

📊 数据流转图#

用户请求 → Controller → UserServiceImpl
1. 校验手机号格式
2. 验证Redis中的验证码
3. 查询/创建用户(多表操作)
4. 生成Token → 存入Redis Hash
5. 维护用户等级集合(可选)
6. 返回Token给前端

📌 总结#

核心围绕 Redis-based Token认证登录即注册 两大核心逻辑,整合了验证码发送、用户签到、等级维护等常用功能。

该类充分体现了分布式项目的设计思想,解决了Session共享、高并发签到、分表存储等企业级常见问题。

(注:文档部分内容可能由 AI 生成)

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Redis为何高性能?
https://mizuki-master-1vt.pages.dev/posts/redis为何高性能/
作者
WangNing
发布于
2026-04-29
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录