使用Redis计数器防止并发请求

业务描述

最近在项目中遇到个问题,短信发送的并发请求漏洞:业务需求是需要限制一个号码一分钟内只能获取一次随机码,之前的实现是短信发送请求过来后,先去数据库查询发送记录,根据上一次的短信发送时间和当前时间比较,如果时间差小于一分钟,则提示短信获取频繁,如果超过一分钟,则发送短信,并记录短信发送日志。

问题分析

短信发送是一个很敏感的业务,上面的实现存在一个并发请求的问题,当同一时间有很多请求过来时,同时去查库,同时获取到上一次发送时间没有,或者已超过一分钟,这时候就会重复发送短信了。

使用Redis解决问题

Redis incr 可以实现原子性的递增,可应用于高并发的秒杀活动、分布式序列号生成等场景。这里我使用它来计数实现一分钟内只接受一次请求。

我们在后台接到短信发送请求后,使用Redis的incr设置一个递增KEY(KEY由固定字符串+手机号码组成),并判断该KEY的数值,如果等于1,说明是第一个请求,我们将该KEY值有效期设置为一分钟;如果该KEY的数值大于1,说明是1分钟内的多次请求,这时我们直接返回短信获取频繁,代码如下:

String redisKey = "SMS_LIMIT_" + smsPhone;
long count = redisTemplate.opsForValue().increment(redisKey, 1);
if (count == 1) {
//设置有效期一分钟
redisTemplate.expire(redisKey, 60, TimeUnit.SECONDS);
}
if (count > 1) {
resultMap.put("retCode", "-1");     resultMap.put("retMsg", "每分钟只能发送一次短信");
outPrintJson(resultMap);
return;
}
/** 发送短信 */
......
/** 记录发送日志 */
......
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

使用java锁机制解决问题

我们知道Java有一个锁机制,使用synchronized关键字可以将对象或代码块锁定,当有很多请求过来时,第一个获得锁,后面的请求需要等第一个处理完后才能继续,就好像排队上厕所。代码如下: 
1、我们创建一个发短信的工具类,使用单例模式(单例模式的兼容效率和并发的写法,参照我前面的博客【善用设计模式-单例模式】)

private static byte[] LOCK = new byte[0];
private static SMSendUtil smSendUtil;
private SMSendUtil(){};
/**
     * @Description:单例 (防并发)
     * @param:@return   
     * @return:SMSendUtil   
     * @throws:
     * @author:pengl
     * @Date:2016年8月17日 下午4:16:08
     */
    public static SMSendUtil getInstance(){
        if(smSendUtil != null)
            return smSendUtil;
        synchronized (LOCK) {
            if(smSendUtil == null)
                smSendUtil = new SMSendUtil();
        }
        return smSendUtil;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2、编写发短信的方法,并在方法上加synchronized关键字

/**
     * 发送登陆验证短信(使用全局锁synchronized避免并发请求)
     */
    public synchronized Map sendSms(String smsPhone) {
        /** 查询最近一次发送时间 **/
        ......
        /** 一分钟时间限制判断 **/
        ......
        /** 发送短信 **/
        ......
        /** 记录发送日志 */
        ......
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

java锁机制优化

在单例模式下,通过在方法上加synchronized关键字,是可以解决并发问题的,但是这种全局锁,性能非常差,如果有100个用户同时获取短信随机码,这100个用户只能排队一个一个发送短信。我们可以使用更细小颗粒的锁来控制并发问题,这里使用手机号码做为颗粒纬度,优化代码如下: 
1、根据手机号码获取相应的锁对象

private static ConcurrentHashMap lockerStore = new ConcurrentHashMap();

private static Object getPhoneNumberLock(long phone) {
        lockerStore.putIfAbsent(phone, new Byte[]{});
        Byte[] ret = lockerStore.get(phone);
        return ret;
}
    /**
    * 发送登陆验证短信(使用号码颗粒级锁synchronized避免并发请求)
    */
    public Map sendSms(String smsPhone,boolean flag) {
    synchronized (getPhoneNumberLock(Long.parseLong(smsPhone))) {
        /** 查询最近一次发送时间 **/
        ......
        /** 一分钟时间限制判断 **/
        ......
        /** 发送短信 **/
        ......
        /** 记录发送日志 */
        ......
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

功能测试

使用Jmeter进行并发测试,同时发送200个线程请求,通过测试发现,以上3种方式均可避免并发请求问题 
使用Redis计数器防止并发请求_第1张图片 
查看结果,只有第一个请求发送了短信,剩余199个请求均返回失败 
使用Redis计数器防止并发请求_第2张图片 
使用Redis计数器防止并发请求_第3张图片

性能测试

使用Jmeter进行并发测试,同时发送200个线程请求,查看聚合报告: 
1、使用Redis计数器方式 
使用Redis计数器防止并发请求_第4张图片 
2、使用全局synchronized 方式 
使用Redis计数器防止并发请求_第5张图片 
3、使用号码颗粒纬度synchronized 方式 
使用Redis计数器防止并发请求_第6张图片

通过测试发现,使用Redis计数器方式,并发性能最好;由于测试是使用相同号码进行测试的,第2、3两种方式测试结果差不多,实际应用中,第三种的性能肯定要高于第二种的。不过总的来说,使用锁机制性能是比较差的。

你可能感兴趣的:(redis)