短信接口防恶意并发请求
问题描述
项目的业务有个功能是发送短信验证码,为了防止该接口功能被恶意调用,需要做出一些限制,比如每个手机号每60秒只能成功执行一次短信接口请求
问题探索解决
设计方案
最开始没有怎么经过思考便决定使用Redis中间件来简单解决这个问题:
当请求进入时,用手机号作为key检测该key是否存在,如果不存在则将该key存入Redis中,设置60秒过期,并正确进入业务;如果该key存在,则返回错误,不给进入短信业务
1 | String validKey = 手机号 + "xxx_xxx" |
在我按这个思路做完简单测试之后,我发现我忽略了这是一个并发问题,上面的方法实现不能保证原子性操作,在我实际测试过程中也证明了这一点,20个线程对接口进行并发测试,20个请求全都正确进入短信业务了,这很显然不符合预期要求
既然是并发问题那就要加锁了,问题是怎么设计才能最小代价最有效的实现
我想了几个方案:
- 最简单最直接,把那一块短信业务的代码用 synchronized 关键字加锁,一次只能进一个线程执行
- 用Redis实现一个简单队列,让请求排队执行,这样就不会有并发问题了
- 用 Map<String, ReentrantLock>实现一个内存锁池,针对不同的手机号进行加锁
- 使用redisTemplate的 increment() 保证操作的原子性
实验
我在探索的过程中用压测软件来实现500个请求的并发量,侧面评估每个方案的可行性。
第一个方案
使用synchronized关键字对整个代码块加锁,实现很简单,也不会出错,但执行效果很差,只能单机服务可以使用,不同手机号之间还会影响,500个请求的执行平均用了8秒多,直接pass
第二个方案
大概想了一下,没有试,因为引入队列之后会带来一系列复杂的问题,并且还要保证队列的消息可靠性,单个线程去消费执行的也很慢,多线程消费又要保证线程之间数据一致,又要加锁,业务搞的太复杂了,不现实
第三个方案
这个实现起来比较简单,使用java的本地锁ReentrantLock,同样只能单机服务使用
1 | private Map<String, ReentrantLock> valueLocks = new ConcurrentHashMap<>(); |
最后统计下来500个线程平均用时0.6秒,比较不错
但有个缺点是需要手动管理60秒过期的锁状态,并且需要使用java虚拟机的内存,可能会影响整体服务
第四个方案
这个最简单,只需要使用redisTemplate的函数,对不同手机号的key进行自增,将原子性操作的加锁,交给Redis解决,如果自增结果大于1则证明发送过了,禁止线程进入
1 | Long num = thirdRedisTemplate.opsForValue().increment(validKey); |
最后测试结果500个请求并发下用时平均0.6-0.7秒,很优秀
和第三个方案比,优点是Redis自动管理key的过期,比较简单方便,缺点是不能设计太复杂的策略,我这个场景下一个key自增就可以解决,但如果需要太复杂的策略,一个自增数解决不了,就不好用了
问题结论
使用redisTemplate的 increment() 原子性操作来解决短信接口防并发爆破最简单快捷,性能也不错