Redis进阶
Redis 事务
相关内容
Redis的事务和 MySQL
的本质上是差不多的,都是保证一组命令要么都执行要么都不执行
Redis
是将多个命令打包,主要用于解决需要批量操作且保证操作完整性的场景。
Redis 事务的设计基于 “命令队列” 和 “单线程执行”
特性,核心原理可以概括为:将一组命令一次性放入队列,在执行时按顺序原子化执行 ,中间不会被其他命令插入。
Redis 事务的执行分为 3 个阶段:开始事务→命令入队→执行 /
放弃事务 ,流程如下:
开始事务 :执行MULTI命令,Redis
进入事务状态。
命令入队:后续输入的命令(如SET、INCR等)不会立即执行,而是被放入事务队列,并返回
“QUEUED” 表示入队成功。
若命令存在语法错误(如参数个数不对),入队时会直接报错,此时事务队列无效,后续EXEC会被忽略。
执行 / 放弃事务:
执行EXEC:Redis
按顺序执行队列中所有命令,返回各命令的结果列表。
执行DISCARD:清空队列,放弃事务,Redis
退出事务状态。
与传统关系型数据库(如 MySQL)的事务不同,Redis
事务的特性有其特殊性:
原子性(Atomicity) :事务中的命令要么全部执行,要么全部不执行(但运行时错误不会导致回滚,这点与传统事务不同)。
一致性(Consistency) :事务执行前后,Redis
的数据状态始终符合预期规则(如数据类型约束、业务逻辑约束)。
隔离性(Isolation) :事务执行期间,不会被其他客户端的命令干扰(因
Redis 单线程执行,天然隔离)。
持久性(Durability) :是否持久化取决于 Redis
的持久化配置(RDB/AOF),事务本身不保证持久性。
与传统事务的关键区别
无回滚机制 :Redis
事务在执行过程中若某条命令发生运行时错误(如对字符串执行自增操作),后续命令仍会继续执行,不会回滚已执行的命令。
轻量设计 :Redis
事务不支持复杂的隔离级别(如读已提交、可重复读),仅通过单线程特性保证隔离性。
Redis 事务的核心命令如下,Redis 提供了 4 个核心命令用于操作事务:
命令
作用
MULTI
标记事务开始,后续命令会被放入事务队列(不会立即执行)。
EXEC
执行事务队列中的所有命令,返回各命令的执行结果(事务结束)。
DISCARD
放弃事务,清空队列中所有命令(事务结束)。
WATCH
监控一个或多个键,若事务执行前被监控的键被修改,则事务会被打断(取消执行)。
其中,WATCH命令是 Redis 实现并发控制的核心,它通过
“乐观锁” 机制解决事务中的竞态条件(多个客户端同时修改同一数据)。
工作原理也比较清楚
WATCH key1 key2 ...:监控指定键,记录其当前版本(Redis
内部通过数据的 “修改次数” 实现)。
当执行EXEC时,Redis
会检查被监控的键是否被其他客户端修改过:
若未被修改,正常执行事务。
若被修改,事务被打断(EXEC返回nil),客户端需重新尝试。
因为 Redis 的事务不能回滚,所以用 Redis
的事务不多,运行时错误需手动处理,而且事务执行时会独占 Redis
线程,若队列命令过多,会阻塞其他请求。
还记得我们之前讲的 Redis 脚本吗,这个可以啊,Redis 会将整个 Lua
脚本作为原子操作执行,支持条件判断,可替代复杂事务。
示例(用 Lua 实现库存扣减):
1 2 3 4 5 6 7 local stock = tonumber (redis.call('get' , KEYS[1 ]) or "0" )if stock > 0 then redis.call('decr' , KEYS[1 ]) return 1 end return 0
实践示例
下面通过具体案例演示 Redis 事务的使用。
示例 1:基本事务操作
假设需求:批量设置两个键值对,并获取结果。那么可以这样输入命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET name "redis" QUEUED 127.0.0.1:6379> SET version "6.2" QUEUED 127.0.0.1:6379> EXEC 1) OK 2) OK 127.0.0.1:6379> GET name "redis" 127.0.0.1:6379> GET version "6.2"
示例 2:放弃事务(DISCARD)
需求:入队命令后,放弃执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET a 10 QUEUED 127.0.0.1:6379> SET b 20 QUEUED 127.0.0.1:6379> DISCARD OK 127.0.0.1:6379> GET a (nil)
示例 3:WATCH 实现乐观锁(库存扣减)
需求:某商品库存为 10,两个客户端同时尝试扣减
1,保证最终库存正确(避免超卖)。
客户端 A 操作 :
1 2 3 4 5 6 7 8 9 10 11 127.0.0.1:6379> WATCH stock OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> DECR stock QUEUED
客户端 B 操作 (在 A 执行EXEC前):
1 2 3 127.0.0.1:6379> DECR stock (integer ) 9
客户端 A 继续执行 :
1 2 3 4 5 6 7 127.0.0.1:6379> EXEC (nil) 127.0.0.1:6379> GET stock "9"
此时客户端 A
需重新执行事务(重新WATCH→MULTI→DECR→EXEC)。
示例 4:运行时错误的处理(无回滚)
需求:事务中某命令执行失败,观察后续命令是否继续执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> INCR name QUEUED 127.0.0.1:6379> SET c 30 QUEUED 127.0.0.1:6379> EXEC 1) (error) ERR value is not an integer or out of range 2) OK 127.0.0.1:6379> GET c "30"
运行时错误不会导致事务回滚,需通过程序逻辑处理(如检查EXEC返回结果)。
Redis之生存时间
相关内容
因为 Redis 通常都是缓存,所以说,我们在进行 Redis
存缓存都需要一个过期功能,所以就需要设置 TTL
Redis 的生存时间(Time To
Live,TTL)是一项重要功能,允许为键设置过期时间,当时间到期后,Redis
会自动删除这些键。这一机制广泛用于缓存过期、临时数据存储、会话管理等场景。下面从原理到实践详细讲解:
生存时间是为 Redis 键设置的
“过期倒计时”,当倒计时结束,键会被自动删除。其核心特点包括:
时效性 :键在指定时间后自动失效,无需手动删除。
原子性 :设置和过期删除操作都是原子性的,不会出现部分执行的情况。
灵活性 :支持设置秒级或毫秒级精度的过期时间,也可动态修改或取消。
Redis 提供了 5 个核心命令用于管理键的生存时间:
命令格式
作用说明
EXPIRE key seconds
为键设置秒级过期时间(倒计时seconds秒后删除)。
PEXPIRE key milliseconds
为键设置毫秒级过期时间(倒计时milliseconds毫秒后删除)。
EXPIREAT key timestamp
为键设置秒级过期时间戳(到达timestamp秒数时删除,基于
Unix 时间戳)。
PEXPIREAT key timestamp
为键设置毫秒级过期时间戳(到达timestamp毫秒数时删除)。
TTL key
查看键的剩余生存时间(秒级,返回 - 1 表示永不过期,-2 表示已过期 /
不存在)。
PTTL key
查看键的剩余生存时间(毫秒级,返回值含义同TTL)。
PERSIST key
移除键的过期时间,使其永不过期(若键存在且有过期时间,返回
1;否则返回 0)。
使用的时候需要注意以下内容:
精度问题 :Redis
的过期时间依赖服务器时间,若服务器时间被修改,可能导致过期异常。
内存占用 :大量过期键未被及时删除可能导致内存溢出,建议合理配置maxmemory-policy(如volatile-lru优先删除过期键)。
持久化影响:
RDB 文件:生成时会过滤已过期的键,加载时也会忽略过期键。
AOF 文件:过期键被删除时,会追加DEL命令到 AOF
文件,保证重放时数据一致。
集群环境 :在 Redis
集群中,过期键的删除由所在节点独立处理,不影响其他节点。
生存时间的工作原理
过期键的存储:
Redis 会将所有设置了过期时间的键单独存储在一个 “过期字典”
中(类似哈希表),字典的键是 Redis
中的实际键,值是该键的过期时间戳(秒或毫秒)。
过期键的删除策略:
Redis
采用三种混合策略 删除过期键,平衡性能和内存占用:
惰性删除:访问键时才检查是否过期,若过期则删除。
优点:不消耗额外 CPU
资源;缺点:可能导致过期键长期占用内存。
定期删除:每隔一段时间(默认
100ms),随机检查部分过期键并删除。
优点:定期释放内存;缺点:可能漏删部分过期键(但会被惰性删除兜底)。
内存淘汰 :当内存达到maxmemory限制时,根据配置的淘汰策略(如allkeys-lru)删除键,包括过期键。
特殊注意事项
对键执行 SET、GETSET
等覆盖操作时,会清除原有的过期时间(键变为永不过期)。
1 2 3 4 5 6 7 8 127.0.0.1:6379> SET name "redis" OK 127.0.0.1:6379> EXPIRE name 100 (integer ) 1 127.0.0.1:6379> SET name "new-redis" OK 127.0.0.1:6379> TTL name (integer ) -1
对键执行INCR、HSET等非覆盖操作时,不会影响其过期时间。
重命名键(RENAME)时,原键的过期时间会转移到新键上。
复制键(COPY)时,默认新键不会继承原键的过期时间(可通过REPLACE参数控制)。
实践示例
示例 1:基本过期时间设置与查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 127.0.0.1:6379> SET session "user123" EX 10 OK 127.0.0.1:6379> TTL session (integer ) 8 127.0.0.1:6379> TTL session (integer ) 3 127.0.0.1:6379> TTL session (integer ) -2 127.0.0.1:6379> GET session (nil)
示例 2:毫秒级精度控制
1 2 3 4 5 6 7 127.0.0.1:6379> PEXPIRE temp 5000 (integer ) 1 127.0.0.1:6379> PTTL temp (integer ) 4892
示例 3:取消过期时间,设置为永久
1 2 3 4 5 6 7 8 9 10 11 12 127.0.0.1:6379> SET cache "data" EX 300 OK 127.0.0.1:6379> TTL cache (integer ) 295 127.0.0.1:6379> PERSIST cache (integer ) 1 127.0.0.1:6379> TTL cache (integer ) -1
示例 4:通过时间戳设置过期时间
1 2 3 4 5 6 7 8 9 10 11 12 127.0.0.1:6379> TIME 1) "1695000000" 2) "123456" 127.0.0.1:6379> EXPIREAT event 1695000060 (integer ) 1 127.0.0.1:6379> TTL event (integer ) 58
常见使用场景
缓存过期管理 :为缓存数据设置过期时间,自动淘汰旧数据(如商品详情缓存
1 小时)。
会话管理 :用户登录会话设置过期时间(如 30
分钟无操作自动登出)。
限时活动 :秒杀活动倒计时、优惠券有效期等场景。
限流控制 :记录接口访问频率,设置短期过期时间(如 1
分钟内最多 100 次请求)。
临时数据存储 :验证码(5
分钟有效)、临时令牌等。
SpringBoot集成Redis
下面我们写一个我正在开发的例子,结合 Spring Security,进行 Redis
存储邮箱验证码的实际使用例子
使用 spring-boot-starter-mail
首先引入依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-mail</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
然后进行如下的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 data: redis: host: localhost port: 6379 password: ${REDIS_PASSWORD} timeout: 2000 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 2
来都来了,也就把邮件的服务配置一下吧,注意,如果你使用的是 QQ
邮箱,需要申请SMTP服务,登陆QQ邮箱「设置」 -
「账户」找到SMTP选项,选择开启服务,生成授权码。
1 2 3 4 5 6 7 8 9 10 11 spring: mail: host: smtp.qq.com port: 587 username: xxxxxxxx password: xxxxxxxx
接下来我们定义操作邮件的 Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import cn.hutool.core.util.StrUtil;import lombok.SneakyThrows;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.FileSystemResource;import org.springframework.mail.javamail.JavaMailSender;import org.springframework.mail.javamail.MimeMessageHelper;import org.springframework.stereotype.Service;import javax.mail.internet.MimeMessage;import java.io.File;@Service public class EmailService { @Autowired private JavaMailSender mailSender; @Value("${spring.mail.username}") private String sendFrom; @SneakyThrows(Exception.class) public void sendSimpleMail (String subject, String content,String... sendTo) { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper (message, true ); messageHelper.setFrom(sendFrom); messageHelper.setTo(sendTo); messageHelper.setSubject(subject); messageHelper.setText(content,true ); mailSender.send(message); } @SneakyThrows(Exception.class) public void sendAttachmentsMail (String subject, String content,String attachmentName, String filePath, String... sendTo) { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper (message, true ); messageHelper.setFrom(sendFrom); messageHelper.setTo(sendTo); messageHelper.setSubject(subject); messageHelper.setText(content,true ); if (StrUtil.isNotBlank(filePath)){ FileSystemResource file = new FileSystemResource (new File (filePath)); attachmentName = StrUtil.isNotBlank(filePath)?attachmentName:filePath.substring(filePath.lastIndexOf(File.separator)); messageHelper.addAttachment(attachmentName, file); } mailSender.send(message); } }
把你的User相关的实体类写好
1 2 3 4 5 6 7 8 9 10 11 12 import lombok.Data;@Data public class User { private Long id; private String username; private String password; private String email; private boolean isActivated; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import lombok.Data;import javax.validation.constraints.Email;import javax.validation.constraints.NotBlank;import javax.validation.constraints.Size;@Data public class UserRegisterDTO { @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; @NotBlank(message = "用户名不能为空") @Size(min = 4, max = 20, message = "用户名长度必须在4-20之间") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, message = "密码长度不能少于6位") private String password; @NotBlank(message = "验证码不能为空") private String verificationCode; }
创建一个控制器处理用户注册和账户激活请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 import org.springframework.web.bind.annotation.*;@RestController public class UserController { @Resource private UserMapper userMapper; @Resource private EmailService emailService; @Resource private VerificationCodeService verificationCodeService; @PostMapping("/send-verification-code") public String sendVerificationCode (@RequestParam String email) { User existingUser = userMapper.getByEmail(email); if (existingUser != null ) { return "该邮箱已被注册" ; } String code = verificationCodeService.generateCode(); verificationCodeService.storeCode(email, code); String subject = "注册验证码" ; String content = "您的注册验证码是: <strong>" + code + "</strong>,有效期5分钟,请尽快完成验证。" ; emailService.sendSimpleMail(subject, content, email); return "验证码已发送到您的邮箱,请查收" ; } @PostMapping("/register") public String registerUser (@RequestBody UserRegisterDTO user) { boolean isCodeValid = verificationCodeService.verifyCode(user.getEmail(), user.getVerificationCode()); if (!isCodeValid) { return "验证码无效或已过期" ; } verificationCodeService.removeCode(user.getEmail()); User newUser = new User (); newUser.setEmail(user.getEmail()); newUser.setUsername(user.getUsername()); newUser.setPassword(user.getPassword()); newUser.setActivated(true ); userMapper.insert(newUser); return "注册成功!" ; } @PostMapping("/forgot-password") public String forgotPassword (@RequestParam String email) { User user = userMapper.getByEmail(email); if (user == null ) { return "该邮箱未注册" ; } String code = verificationCodeService.generateCode(); verificationCodeService.storeCode("reset:" + email, code); String subject = "密码重置验证码" ; String content = "您的密码重置验证码是: <strong>" + code + "</strong>,有效期5分钟,请尽快完成验证。" ; emailService.sendSimpleMail(subject, content, email); return "密码重置验证码已发送到您的邮箱,请查收" ; } @PostMapping("/reset-password") public String resetPassword (@RequestParam String email, @RequestParam String verificationCode, @RequestParam String newPassword) { boolean isCodeValid = verificationCodeService.verifyCode("reset:" + email, verificationCode); if (!isCodeValid) { return "验证码无效或已过期" ; } User user = userMapper.getByEmail(email); user.setPassword(newPassword); userMapper.updatePassword(user); verificationCodeService.removeCode("reset:" + email); return "密码重置成功" ; } }
欸,是不是差了点什么,没错,到这里,Spring Boot 结合
邮件发送的开发完成了,接下来,我们编写一个验证码相关的服务,来体现 redis
在其中的作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.Random;import java.util.concurrent.TimeUnit;@Service public class VerificationCodeService { private final StringRedisTemplate redisTemplate; private static final int CODE_EXPIRATION_MINUTES = 5 ; private static final String CODE_KEY_PREFIX = "verify:code:" ; public VerificationCodeService (StringRedisTemplate redisTemplate) { this .redisTemplate = redisTemplate; } public String generateCode () { Random random = new Random (); StringBuilder code = new StringBuilder (); for (int i = 0 ; i < 6 ; i++) { code.append(random.nextInt(10 )); } return code.toString(); } public void storeCode (String email, String code) { String key = CODE_KEY_PREFIX + email; redisTemplate.opsForValue().set(key, code, CODE_EXPIRATION_MINUTES, TimeUnit.MINUTES); } public String getCode (String email) { String key = CODE_KEY_PREFIX + email; return redisTemplate.opsForValue().get(key); } public boolean verifyCode (String email, String inputCode) { String storedCode = getCode(email); if (storedCode == null ) { return false ; } return storedCode.equals(inputCode); } public void removeCode (String email) { String key = CODE_KEY_PREFIX + email; redisTemplate.delete(key); } }
可以看到,最后验证码具有实际开发使用的优势,在于 redis 给的
自动过期 :无需手动清理过期验证码,Redis
会自动删除过期数据
分布式支持 :在集群环境下,Redis
可以作为中央存储,确保验证码在多个服务实例间共享
高性能 :Redis
的读写性能远高于数据库,适合存储这类高频访问的临时数据
减轻数据库负担 :避免使用数据库存储临时验证码数据
基于Redis的分布式锁
基于 Redis
的分布式锁是解决分布式系统中资源竞争问题的常用方案,尤其适用于多个服务实例需要共享资源(如你案例中的邮件验证码发送限制、用户注册并发控制等场景)。
在单机应用中,我们可以用synchronized或Lock实现线程同步,但在分布式系统(多服务实例)中,这些本地锁会失效。Redis
分布式锁的核心思路是:
多个服务实例通过 Redis 的原子操作 竞争同一个
“锁键”
谁能成功创建这个锁键,谁就获得锁,执行临界操作
操作完成后释放锁,其他实例才能竞争
核心要求:
互斥性 :同一时间只有一个实例能获得锁
安全性 :锁必须被持有它的实例释放,防止误释放
可用性 :锁最终必须能被释放(如设置过期时间)
Redis 分布式锁的实现:
获取锁 :使用SET key value NX PX timeout命令
NX:只有键不存在时才设置成功(保证互斥)
PX timeout:自动过期时间(防止死锁)
value:建议用
UUID,作为释放锁的唯一标识(防止误释放)
释放锁 :必须校验 value 是否匹配,再删除键(需用
Lua 脚本保证原子性)
1 2 3 4 5 if redis.call('get' , KEYS[1 ]) == ARGV[1 ] then return redis.call('del' , KEYS[1 ]) else return 0 end
我们改造上述的例子,来体现结合邮件验证码案例如何实现分布式锁
以 “限制同一邮箱 1 分钟内最多发送 3 次验证码”
为例,创建分布式锁工具类(RedisDistributedLock)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.stereotype.Component;import java.util.Collections;import java.util.UUID;import java.util.concurrent.TimeUnit;@Component public class RedisDistributedLock { private final StringRedisTemplate redisTemplate; private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" ; public RedisDistributedLock (StringRedisTemplate redisTemplate) { this .redisTemplate = redisTemplate; } public String tryLock (String lockKey, long expireTime) { String lockValue = UUID.randomUUID().toString(); Boolean success = redisTemplate.opsForValue().setIfAbsent( lockKey, lockValue, expireTime, TimeUnit.SECONDS ); return success != null && success ? lockValue : null ; } public boolean unlock (String lockKey, String lockValue) { DefaultRedisScript<Long> script = new DefaultRedisScript <>(UNLOCK_SCRIPT, Long.class); Long result = redisTemplate.execute( script, Collections.singletonList(lockKey), lockValue ); return result != null && result > 0 ; } }
tryLock:通过setIfAbsent实现原子性加锁,返回唯一标识
unlock:通过 Lua 脚本保证 “校验 + 删除”
的原子性,防止误释放其他实例的锁
按照上面的redis分布式锁,改造我们的验证码发送服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.Random;import java.util.concurrent.TimeUnit;@Service public class VerificationCodeService { private final StringRedisTemplate redisTemplate; private final RedisDistributedLock distributedLock; private static final String CODE_PREFIX = "verify:code:" ; private static final String SEND_COUNT_PREFIX = "verify:count:" ; private static final String LOCK_PREFIX = "lock:verify:" ; public VerificationCodeService (StringRedisTemplate redisTemplate, RedisDistributedLock distributedLock) { this .redisTemplate = redisTemplate; this .distributedLock = distributedLock; } public String generateAndSendCode (String email, EmailService emailService) { String lockKey = LOCK_PREFIX + email; String lockValue = null ; try { lockValue = distributedLock.tryLock(lockKey, 10 ); if (lockValue == null ) { return "操作太频繁,请稍后再试" ; } String countKey = SEND_COUNT_PREFIX + email; String countStr = redisTemplate.opsForValue().get(countKey); int count = countStr == null ? 0 : Integer.parseInt(countStr); if (count >= 3 ) { return "1分钟内最多发送3次验证码" ; } String code = generateCode(); String codeKey = CODE_PREFIX + email; redisTemplate.opsForValue().set(codeKey, code, 5 , TimeUnit.MINUTES); redisTemplate.opsForValue().increment(countKey); redisTemplate.expire(countKey, 1 , TimeUnit.MINUTES); emailService.sendSimpleMail("验证码" , "您的验证码是:" + code, email); return "验证码发送成功" ; } finally { if (lockValue != null ) { distributedLock.unlock(lockKey, lockValue); } } } private String generateCode () { return String.format("%06d" , new Random ().nextInt(999999 )); } public boolean verifyCode (String email, String code) { String codeKey = CODE_PREFIX + email; String storedCode = redisTemplate.opsForValue().get(codeKey); if (code.equals(storedCode)) { redisTemplate.delete(codeKey); return true ; } return false ; } }
我们优化了验证码服务
对同一邮箱 加锁,防止并发请求绕过频率限制
用 Redis 计数器限制 1 分钟内最多发送 3 次验证码
锁的过期时间(10 秒)远大于业务执行时间(发送邮件通常 < 3
秒),避免死锁
实际上,Redisson 框架,自带分布式所,而且还是很牛逼的锁,真正在 redis
中执著锁的不是很多,把锁从数据库层面改造到代码层面往往更加靠谱和更加清楚