發送短信--使用Redis限制發送頻率和日發送次數
來自: http://iamlbk.github.io/blog/20160302/sms-java-code-with-redis/
在前幾篇文章中, 我們介紹了限制發送短信頻率, 限制日發送次數等功能. 但是后來 z-oneC 說用Redis實現會更簡單. 于是這幾天我大致學了一下Redis, 然后使用Redis重新實現了一次. 當然由于剛接觸Redis, 或許有些地方并不合適, 還請您在留言區留言, BK在這里先謝過了.
其實使用Redis確實挺簡單, 至少沒有過于復雜的概念, 龐大的命令集. 基本上入門挺快的. 剩下的就是創造力和經驗了. 這里我們使用Redis來完成前兩篇:《 發送短信–限制發送頻率 》、《 發送短信–限制日發送次數 》完成的功能.
當然, 如果讀者并沒有學過Redis, 可以參見《 The Little Redis Book 》快速入門,這本"書"基本上半個上午就可以看完.
思路
這里我們就是簡單用Redis限制"訪問"頻率:
- 首先根據用戶手機號/IP拼湊出一個字符串的關鍵字.
- 然后判斷該字符串的值是否為空.
- 如果為空, 則設置該字符串的值為1, 并設置生存時間. 并允許"訪問".
- 如果不為空, 則將值加一, 然后判斷值是否超過使用期限時間內的最大"訪問"次數
- 如果沒有超過, 則允許"訪問"
- 否則拒絕"訪問"
編寫腳本
注: 該腳本摘自《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的特性, 如果指定的鍵并不存在, 則默認為0, 并加一. 這一步相當于判斷指定的鍵是否存在, 如果不存在, 則置指定的鍵為1; 否則加一
- 接著判斷是否是第一次訪問, 如果是, 則設置生存時間
- 最后判斷是否超過了訪問頻率, 如果超過了訪問頻率, 則返回0; 否則返回1
使用Jedis調用腳本
在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應該會簡單些.