一、需求场景
对一个忘记密码进行邮件发送功能,同一个账号,两分钟内不能发送第二封邮件。这个可以通过redis进行次数限制。思路为:在代码的随后,随便放一个值,时间设置两分钟,然后在代码的最前面,通过redis取出值,如果不为空,那就说明已经发送过一次。
二、出现问题
在网络出现异常的情况下,用户狂点忘记密码进行邮箱发送,出现一种情况就是,请求在同一时间内进入接口,当一个请求进到接口,未进行到代码最后,记录次数到redis中时,就有第二个,第三个请求进来。这个时候次数限制就会出问题。
三、解决方式
一、使用单体应用锁
单体应用锁指的是只能在 一个JVM 进程内有效的锁。我们把这种锁叫做单体应用锁。
单体应用锁是JDK提供的锁,这种锁只能在 一个JVM 下起到作用,
也就是在一个Tomcat内是没有问题的。当存在两个或两个以上的Tomcat时,大量的并发请求分散到不同的Tomcat上,在每一个Tomcat中都可以防止并发的产生,但是在多个Tomcat之间,每个Tomcat中获得锁的这个请求,又产生了并发。这也就是单体应用锁的局限性了,它只能在一个JVM内加锁,而不能从这个应用层面去加锁。
二、使用分布式锁
单体应用锁是在一个JVM进程内有效,无法跨JVM、跨进程。那么分布式锁的定义就出来了,分布式锁就可以跨越多个JVM、跨多个进程的锁,这种锁就叫做分布式锁
1、使用Redisson作为分布式锁,引入对应依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
public void test() {
// 1.获取锁,没有获取到锁的会阻塞
// 2.redisson设置一个key的默认过期时间为30s
// 3.redisson会自动续期
//设置lockey
String lockKey = "test";
RLock lock = redisson.getLock(lockKey);
//上锁
/**
* 处理业务执行时间大于锁的时间,自动续期
* 不设置过期时间,默认锁的时间为30s,每1/3的时间就自动续期,业务处理完需要手动释放锁
*/
lock.lock();
//lock.lock(10,TimeUnit.SECONDS); 这种是10秒后锁自动过期,不会有自动续期的机制
//boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
try {
//模拟业务执行
Thread.sleep(40000);
log.info("模拟业务执行了40s");
} catch (Exception e){
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
}
lock.lock(); 是阻塞式等待的,默认加锁时间是30s;如果业务超长,运行期间会自动续期到30s。不用担心业务时间长,锁自动过期被删掉;加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题;
也可以自己指定解锁时间lock.lock(10,TimeUnit.SECONDS),10秒钟自动解锁,自己指定解锁时间redis不会自动续期;
2、本次是使用UUID+线程名作为分布式锁的key
String redissionKeyValue = SystemTplConst.FORGOT_PASSWORD+"_"+UUID.randomUUID().toString()+Thread.currentThread().getName();
出现问题是使用Jemeter进行压测,同一秒内,打入50个请求,出现每一个请求都能发出邮件。出现这个问题主要是使用的的key有问题,出现每一个请求的key都不一样,也就是说同一个邮箱请求50次,50次的key都不一样,这就导致请求都进入了代码里,相当于没有加锁。最后改为邮箱为锁的key才解决。也就是同一个邮箱,50次请求,然后key是一样的,就回锁住,第一次请求处理完后。第二个请求才会进去。而这个时候redis就有值了。