【已解决】Java 项目中利用 Redis 配合 Lua 脚本对短信推送消息做推送限制

🎉场景介绍:在实际应用场景中,我们通常需要对短信推送消息做发送限制的处理,避免一定时间间隔内发送过多相同内容的推送短信对用户造成骚扰,也要防止攻击者恶意调用短信推送接口造成短信资源的极大浪费。尽管部分三方接口提供方的原生接口就已经做了限制,但是为了能够更好地满足定制化需求以及在编码过程中的疏忽造成接口重复调用导致资源浪费,因此我们需要在项目中做短信推送限制的处理。

💡方案设计

  • 首先,将需要推送的短信内容以及接收短信的手机号进行拼接成新的字符串,并对该字符串进行加密处理转换成16进制字符串,作为存入到 redis 中的 key.
  • 其次,执行 Lua 脚本,该脚本用于在 redis 中存储具有过期时间的 key,并返回重复校验的结果。
  • 最后,获取校验结果,根据校验结果进行相应的处理操作,如果是重复发送的短信就将该接口和手机号加入数据埋点,用于监控推送限制情况,并抛出重复发送的异常;否则,正常发送短信消息。

在对项目中的业务代码做了抽离后,只保留了和方案设计相关的代码,如下所示

 public class SmsService {
	
	private static Logger logger = LoggerFactory.getLogger(SmsService.class);

	@Autowired
    private JedisTemplate jedisTemplate;

	@Autowired
	private MeterRegistry meterRegistry;

    private static final String LIMIT_LUA = "local num = redis.call('incr', KEYS[1])\n" +
            "if num == 1 then\n" +
            "      redis.call('expire', KEYS[1], ARGV[1])\n" +
            "end\n" +
            "if num > tonumber(ARGV[2]) then\n" +
            "      return 0\n" +
            "end\n" +
            "return 1";
 
 	/**
     * 发送短信
     * @param sendSmsRequest 发送短信请求信息
     * @return 
     */
    @Transactional
    public String sendSms(SendSmsRequest sendSmsRequest) {
    	String key = sendSmsRequest.getPhoneNumber() + sendSmsRequest.getTemplateCode() + sendSmsRequest.getTemplateParam();
		key = DigestUtils.md5Hex(key);
        // 60秒内同一手机号相同内容只允许发送两次
        boolean limitFlag = checkIsLimited(key, 60L, 2L);
        if (limitFlag) {
            logger.warn("短信推送被限制,sendSmsRequest:{}", JsonTool.serialize(sendSmsRequest));
            // 加入数据埋点,用于监控超出推送限制的情况
            meterRegistry.counter("sms.repeat.limit.count", "/test/sms", uri, sendSmsRequest.getPhoneNumber(), user).increment();
            throw new ServiceException("短信推送超出限制");
        }
        
		// 推送短信,次数省略100w行代码...
     }
     
    /**
     * 推送频率限制,seconds内只允许limitNum次
     * @param key key
     * @param seconds 时间 秒
     * @param limitNum 限制次数
     * @return true:限制 false:不限制
     */
    public boolean checkIsLimited(String key, long seconds, long limitNum) {
        try {
            Long result = (Long) jedisTemplate.eval(LIMIT_LUA, CoreTool.array2List(key), CoreTool.array2List(String.valueOf(seconds), String.valueOf(limitNum)));
            return result == 0;
        } catch (Exception e) {
            logger.warn("校验是否需要推送限制异常", e);
        }
        	return false;
    	}
	}
}

其中,对上述 Lua 脚本功能具体的注释如下:

local num = redis.call('incr', KEYS[1])  -- 计数加1(第一次调用时,key不存在,默认会创建并赋值1)
if num == 1 then                         -- 第一次调用
	redis.call('expire', KEYS[1], ARGV[1])  -- 对key设置过期时间
end
if num > tonumber(ARGV[2]) then          -- 非第一次访问,判断是否在过期时间内大于限定的访问次数
	return 0
end
return 1
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ReadThroughLife

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值