發送短信--使用Redis限制發送頻率和日發送次數

rige7001 8年前發布 | 25K 次閱讀 Redis NoSQL數據庫

來自: 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入門指南》

ratelimiting.lua
--[[ 實現訪問頻率的腳本. 參數:  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

RateLimit.java
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調用腳本. 獲取返回值, 判斷頻率是否過高.

使用示例

下面我們使用上面的代碼完成限制發送頻率的功能(部分接口和類的聲明請參見《 發送短信–限制發送頻率 》). 限制日發送次數的代碼基本相同, 這里就不貼了, 請下載源碼查看.

FrequencyFilter
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應該會簡單些.

 本文由用戶 rige7001 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!