在前几篇文章中, 我们介绍了限制发送短信频率, 限制日发送次数等功能. 但是后来 z-oneC 说用Redis实现会更简单. 于是这几天我大致学了一下Redis, 然后使用Redis重新实现了一次. 当然由于刚接触Redis, 或许有些地方并不合适, 还请您在留言区留言, BK在这里先谢过了.
其实使用Redis确实挺简单, 至少没有过于复杂的概念, 庞大的命令集. 基本上入门挺快的. 剩下的就是创造力和经验了. 这里我们使用Redis来完成前两篇:《 发送短信–限制发送频率 》、《 发送短信–限制日发送次数 》完成的功能.
当然, 如果读者并没有学过Redis, 可以参见《 The Little Redis Book 》快速入门,这本"书"基本上半个上午就可以看完.
这里我们就是简单用Redis限制"访问"频率:
--[[ 实现访问频率的脚本. 参数: KEY[1] 用来标识同一个用户的id ARGV[1] 过期时间 ARGV[2] 过期时间内可以访问的次数 返回值: 如果没有超过指定的频率, 则返回1; 否则返回0 ]] local times = redis.call('incr', KEYS[1]) if times == 1 then -- 说明刚创建, 设置生存时间 redis.call('expire', KEYS[1], ARGV[1]) end if times > tonumber(ARGV[2]) then return 0 end return 1
该脚本也比较直观:
在Redis的 官网 上有许多Redis的 Java客户端的库 . 这里我们使用 Jedis .
我们来看看代码. 该程序中的 ClassPathResource
和 FileCopyUtils
类为Spring中的类, 因此这里的示例程序依赖于Spring
public class RateLimit { private JedisPool jedisPool; private String script; // 省略了构造方法 public void init() throws Exception { ClassPathResource resource = new ClassPathResource("script/ratelimiting.lua"); script = FileCopyUtils.copyToString(new EncodedResource(resource, "UTF-8").getReader()); } /** * 提供限制速率的功能 * * @param key 关键字 * @param expireTime 过期时间 * @param count 在过期时间内可以访问的次数 * @return 没有超过指定次数则返回true, 否则返回false */ public boolean isExceedRate(String key, long expireTime, int count) { List<String> params = new ArrayList<>(); params.add(Long.toString(expireTime)); params.add(Integer.toString(count)); try(Jedis jedis = jedisPool.getResource()) { List<String> keys = new ArrayList<>(1); keys.add(key); Long canSend = (Long) jedis.eval(script, keys, params); return canSend == 0; } } }
这里的 init
方法的作用就是将刚才我们写的脚本读取到 script
变量中, 以便以后使用.
isExceedRate
方法将关键字和参数(过期时间和发送次数)分别封装到 List
里, 之后使用Jedis调用脚本. 获取返回值, 判断频率是否过高.
下面我们使用上面的代码完成限制发送频率的功能(部分接口和类的声明请参见《 发送短信–限制发送频率 》). 限制日发送次数的代码基本相同, 这里就不贴了, 请下载源码查看.
public class FrequencyFilter implements SmsFilter { private static final String KEY_PREFIX = "rate.frequency.limiting:"; private RateLimit rateLimit; private int sendInterval; // 省略了部分代码 @Override public void filter(Sms sms) throws FrequentlyException { if(rateLimit.isExceedRate(KEY_PREFIX+sms.getMobile(), sendInterval, 1) || rateLimit.isExceedRate(KEY_PREFIX+sms.getIp(), sendInterval, 1)){ throw new FrequentlyException("发送短信过于频繁"); } } }
到这里我们的主要代码就完成了, 可以看出使用Redis后代码确实非常的简单.
由于我现在还不会性能测试, 所以只是简单的使用 for
循环测试了一下性能, 虽然可能不是很准确, 但是也有一定的可信度. 在限制发送频率时, 使用 ConcurrentMap
的性能更高, 貌似比例还不小, 只是由于基数并不大, 所以并没有多费多少时间(十万条记录只多花费了十五秒). 但是在限制日发送次数时, 剩下了n多时间. 综合来看, 还是只使用Redis更省时省事. 而且, 个人猜测, 在扩展到集群时, 使用Redis应该会简单些.