一次使用Redis做短信次数验证的小思考

·  阅读 2508

前文

都好久没写博文了,一转眼都一个月过去了,今天写点儿简单的,是我工作中遇到的,前一段时间做一个任务,里面涉及到一个发送短信次数的验证:

需求

原需求是这样的:

有一个体现体统,每次体验需要短信做验证,PM提出一个要求 当用户每小时发送提现短信次数超过5次,超过了就当天限制发送!

想法

拿到这样的需求,各位小伙伴会有怎么样的想法呢,你心中的方案是什么?

话不多说!

我一开始的第一个想到的方案 是 存数据库 建一张表 记录发送的记录 然后每次发送的时候 查表再做下验证!为嘛为会这么想呢,因为我看系统里面之前发送短信频次的验证就是这么写的!

如果这样时候 我就这么写 也就没今天的博文了,后来我看了下 如果是为了验证发送短信频次 就存数据库,感觉有点不妥哈前提说下,我们的系统是多负载的,也就是说 通过HashMap,或者list 是没法解决的于是我就想到了Redis!

一次失败的使用

下面先看下 我第一次的方案:

private boolean LimitSendSmsFirst(String user, long maxSendCountForHour) {
        String redisKey = String.format("LimitSend-%s", user);
        long keyCount = redisTemplate.opsForValue().increment(redisKey, 1);
        if (keyCount == 1) {
            redisTemplate.expire(redisKey, (60 * 60), TimeUnit.SECONDS);
        }
        if (keyCount > maxSendCountForHour) {
            System.out.printf("当前用户 %s 发送短信已经超每小时 %d 次上限", user, maxSendCountForHour);
            return false;
        }
        return true;
 }
复制代码

参数说明:

  • user 就是发送的用户,可以使用用户ID,手机号码等 代表发送用户标识的参数
  • maxSendCountForHour 就是每小时最大可以发送的次数

咋一看 我这代码写的很棒

首先我使用了一个原子命令 每次调用的时候次数加1,这儿说明下increment 如果key不存在的话 默认的值是0,那调用过一次后 得到的值就是1了,因为我每次都是加1的!

这样我在第一次使用的时候,设置当前的key 是1个小时,因为我要判断1个小时内 发送的次数嘛,这样做没毛病!~

下面就是 我判断当前的发送次数 如果是发送次数已经大于设置的超限数组,我就返回发送失败! 否则就成功!

于是 我很完美的 完成了~

不知道 看到这里 有没有小伙伴 发现这个代码验证逻辑是有问题的~

bibibibibibi

嗯嗯 让我想一下!!!

bibibibibibi

好吧!!!

想不出来 那我们就来测试下:

@Test
    public void testLimitSendSMS() throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            String user = "burgxun";
            System.out.printf("当前用户 %s 开始发送短信。。。\n", user);
            boolean isOK = LimitSendSmsFirst(user, 5);
            System.out.printf("用户 %s 发送短信成功\n", user);
            Thread.sleep(1000);
        }
    }
    private boolean LimitSendSmsFirst(String user, long maxSendCountForHour) {
        String redisKey = String.format("LimitSendFirst-%s", user);
        long keyCount = redisTemplate.opsForValue().increment(redisKey, 1);
        if (keyCount == 1) {
            redisTemplate.expire(redisKey, (10), TimeUnit.SECONDS);
        }
        if (keyCount > maxSendCountForHour) {
            System.out.printf("当前用户 %s 发送短信已经超每小时 %d 次上限", user, maxSendCountForHour);
            return false;
        }
        return true;
    }
复制代码

我这边修改了下代码 没秒钟发送一次,限制10秒钟之内只能发送5次!

看下代码的执行结果:

当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。当前用户 burgxun 发送短信已经超每小时 5 次上限
复制代码

看执行的很完美~ 10秒种只能发送5次成功的~

但是当我修改下代码的,如果是这样执行的话:

    @Test
    public void testLimitSendSMS() throws InterruptedException {
        Integer[] arr = new Integer[]{1, 8, 9, 10, 11, 12, 13};
        List<Integer> integerList = Arrays.asList(arr);
        for (int i = 0; i < 20; i++) {
            if (integerList.contains(i)) {
                String user = "burgxun";
                System.out.printf("当前用户 %s 开始发送短信。。。,当前发送的时间是:%s ", user, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                boolean isOK = LimitSendSmsLast(user, 5);
                if (isOK) {
                    System.out.printf("用户 %s 发送短信成功\n", user);
                }
            }
            Thread.sleep(1000);
        }
    }
复制代码

执行结果如下:

当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:36:05 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:36:12 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:36:13 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:36:14 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:36:15 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:36:16 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:36:17 用户 burgxun 发送短信成功
复制代码

看下 现在的执行结果 这样显然是不正确的,因为 这边存在一个临界问题,你去用一段时间内的数量做判断的时候,这么想本身就已经发生了错误,因为一段时间的这个开始状态 的时间 其实是在变化的~我们不能设置一个固定的开头 去统计!

其实这样的想法,我在一开始做的时候 和小伙伴讨论的时候么!小伙伴也是这样想到,用了一个固定的开始去统计这个次数!

修改

那既然知道了 问题的所在~ 那我们怎么去解决呢! 我是这样想的 既然 只能每个小时发送5条,那我就是要判断下 第一条和第5条相隔的时间 如果时间间隔大于我设置的时间,那就限制发送! 如果这样的花 我就要一个容器去存储发送的数据,而且还要存储发送的时间,这里我用了Redis里面的List的数据结构!

下面来去看下我的实现:

Show code

    @Test
    public void testLimitSendSMS() throws InterruptedException {
        Integer[] arr = new Integer[]{1, 8, 9, 10, 11, 12, 13};
        List<Integer> integerList = Arrays.asList(arr);
        for (int i = 0; i < 20; i++) {
            if (integerList.contains(i)) {
                String user = "burgxun";
                System.out.printf("当前用户 %s 开始发送短信。。。,当前发送的时间是:%s ", user, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                boolean isOK = LimitSendSmsLast(user, 5);
                if (isOK) {
                    System.out.printf("用户 %s 发送短信成功\n", user);
                }
            }
            Thread.sleep(1000);
        }
    }

    private boolean LimitSendSmsLast(String user, long maxSendCountForHour) {
        String canSendRedisKey = String.format("CanSendSms-%s", user);
        String isCanSendSms = stringRedisTemplate.opsForValue().get(canSendRedisKey);
        if (isCanSendSms != null && isCanSendSms.equals("1")) {
            System.out.printf("当前用户 %s 发送短信已经超每小时 %d 次上限\n", user, maxSendCountForHour);
            return false;
        }
        String redisKey = String.format("LimitSendLast-%s", user);
        long keyCount = redisTemplate.opsForList().size(redisKey);
        if (keyCount < maxSendCountForHour) {
            long redisTimeValue = System.currentTimeMillis();
            redisTemplate.opsForList().leftPush(redisKey, redisTimeValue);
        } else {
            long value = (long) redisTemplate.opsForList().rightPop(redisKey);
            long timeInterval = (System.currentTimeMillis() - value) / (1000);
            if (timeInterval > 10) {
                System.out.printf("当前用户 %s 发送短信已经超每小时 %d 次上限\n", user, maxSendCountForHour);
                stringRedisTemplate.opsForValue().set(canSendRedisKey, "1");
                return false;
            }
        }
        return true;
    }
复制代码

执行的结果:

当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:38:11 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:38:19 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:38:20 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:38:21 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:38:22 用户 burgxun 发送短信成功
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:38:23 当前用户 burgxun 发送短信已经超每小时 5 次上限
当前用户 burgxun 开始发送短信。。。,当前发送的时间是:2020-08-22 18:38:24 当前用户 burgxun 发送短信已经超每小时 5 次上限
复制代码

看下这边的代码,我的思路是用了一个String类型的RedisKey去存储当前用户是否能发送短信,那这个值得赋值逻辑就是当此用户发送超限的时候就设置为1

我用一个List去存储发送的记录,List里面的value存储的时候发送的时候时间戳,首先我判断了下当前list里面的数量 如果小于超限的次数 就往List中去左边存,如果大于或者等于了 我就 从List右边取出,然后对比下取出的时间和当前的时间,如果是大于了设置的时间间隔 就设置此用户 不能再次发送!当然这边限制的时间 还是要改下的 我的需求是当天不能在发送,所以这个有效期还是要设置一下的~

总结

也许这个只是一个小的问题,可能很多人也会在工作中选择了存数据库去解决了可能也有别的更优美的方案上面只是我个人的个想法过程,也不一定是最完美的,只是当我们遇到这样的问题的时候,我们思考的角度 很重要,不然我们很容易就去选择了 我上面的错误方案,用一个固定的时间端去统计~

在老钱的Redis树中,也描述过一个滑动窗口的概念 其实感觉 和这个有点类似,只是统计的方式有差别,但是核心想法都是差不多的~ 有兴趣的可以去找相关文章看看~

Redis 存在在5种数据结构,每个数据结构 都有它的用途,但是我们经常只知道把其当作缓存使用,或者仅仅使用了String 类型是数据结构,后面我会写一篇 用Redis做一个延迟队列 怎么去实现,这也是我之前工作中做过的~

本文的想法是来自这边:

blog.csdn.net/tianyaleixi…

最后来碗鸡汤:

什么是危机?

真正的危机,来源于在正确的时间做不正确的事。没有在正确的时间,为下一步做出积累,这才是危机的根源。

如果你正在这条成长路上的朋友,晚醒不如早醒,这就是我想说的。千万别等到中年才发现自己没有建立好自己的护城河,这个时候才知道努力。在自己努力的阶段,不仅不努力反了选择了纵容自己,这才是危机的根源。

希望大家会有所收获,不负时光,不负卿!

分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改