java發(fā)送短信系列之限制發(fā)送頻率
本篇是發(fā)送短信的第二部分, 這里我們介紹一下如何限制向同一個(gè)用戶(根據(jù)手機(jī)號(hào)和ip)發(fā)送短信的頻率。
1、使用session
如果是web程序, 那么在session中記錄上次發(fā)送的時(shí)間也可以, 但是可以被繞過去. 最簡(jiǎn)單的, 直接重啟瀏覽器 或者 清除cache等可以標(biāo)記session的數(shù)據(jù), 那么就可以繞過session中的記錄. 雖然很多人都不是計(jì)算機(jī)專業(yè)的, 也沒學(xué)過這些. 但是我們需要注意的是, 之所以限制發(fā)送頻率, 是為了防止"短信炸彈", 也就是有人惡意的頻繁的請(qǐng)求向某個(gè)手機(jī)號(hào)碼發(fā)送短信. 所以這個(gè)人是有可能懂得這些知識(shí)的.
下面我們使用"全局"的數(shù)據(jù)限制向同一個(gè)用戶發(fā)送頻率. 我們先做一些"準(zhǔn)備"工作
2、定義接口、實(shí)體類
我們需要的實(shí)體類如下:
SmsEntity.java
public class SmsEntity{ private Integer id; private String mobile; private String ip; private Integer type; private Date time; private String captcha; // 省略構(gòu)造方法和getter、setter方法 }
過濾接口如下:
SmsFilter.java
public interface SmsFilter { /** * 初始化該過濾器 */ void init() throws Exception; /** * 判斷短信是否可以發(fā)送. * @param smsEntity 將要發(fā)送的短信內(nèi)容 * @return 可以發(fā)送則返回true, 否則返回false */ boolean filter(SmsEntity smsEntity); /** * 銷毀該過濾器 */ void destroy(); }
3、主要代碼
限制發(fā)送頻率, 需要記錄某個(gè)手機(jī)號(hào)(IP)及上次發(fā)送短信的時(shí)間. 很適合Map去完成, 這里我們先使用ConcurrentMap實(shí)現(xiàn):
FrequencyFilter.java
public class FrequencyFilter implements SmsFilter { /** * 發(fā)送間隔, 單位: 毫秒 */ private long sendInterval; private ConcurrentMap<String, Long> sendAddressMap = new ConcurrentHashMap<>(); // 省略了部分無用代碼 @Override public boolean filter(SmsEntity smsEntity) { if(setSendTime(smsEntity.getMobile()) && setSendTime(smsEntity.getIp())){ return true; } return false; } /** * 將發(fā)送時(shí)間修改為當(dāng)前時(shí)間. * 如果距離上次發(fā)送的時(shí)間間隔大于{@link #sendInterval}則設(shè)置發(fā)送時(shí)間為當(dāng)前時(shí)間. 否則不修改任何內(nèi)容. * * @param id 發(fā)送手機(jī)號(hào) 或 ip * @return 如果成功將發(fā)送時(shí)間修改為當(dāng)前時(shí)間, 則返回true. 否則返回false */ private boolean setSendTime(String id) { long currentTime = System.currentTimeMillis(); Long sendTime = sendAddressMap.putIfAbsent(id, currentTime); if(sendTime == null) { return true; } long nextCanSendTime = sendTime + sendInterval; if(currentTime < nextCanSendTime) { return false; } return sendAddressMap.replace(id, sendTime, currentTime); } }
這里, 主要的邏輯在setSendTime方法中實(shí)現(xiàn):
第25-28行: 首先假設(shè)用戶是第一次發(fā)送短信, 那么應(yīng)該把現(xiàn)在的時(shí)間放到sendAddressMap中. 如果putIfAbsent返回null, 那么說明用戶確實(shí)是第一次發(fā)送短信, 而且現(xiàn)在的時(shí)間也已經(jīng)放到了map中, 可以發(fā)送.
第30-33行: 如果用戶不是第一次發(fā)送短信, 那么就需要判斷上次發(fā)送短信的時(shí)間和現(xiàn)在的間隔是否小于發(fā)送時(shí)間間隔. 如果小于發(fā)送間隔, 那么不能發(fā)送.
第35行: 如果時(shí)間間隔足夠大, 那么需要嘗試著將發(fā)送時(shí)間設(shè)置為當(dāng)前時(shí)間.
- 如果替換成功, 那么可以發(fā)送短信.
- 如果替換失敗, 說明有另外一個(gè)線程在本線程執(zhí)行26-35行之間已經(jīng)進(jìn)行了替換, 也就是說在剛才已經(jīng)發(fā)送了一次短信.
1)、那么可以再重復(fù)執(zhí)行25-35行, 確保絕對(duì)正確.
2)、也可以直接認(rèn)為不能發(fā)送, 因?yàn)殡m然理論上"執(zhí)行26-35行"的時(shí)間可能大于"發(fā)送間隔", 但是概率有多大呢? 基本上可以忽略了吧.
這段代碼算是實(shí)現(xiàn)了頻率的限制, 但是如果只有"入"而沒有"出"那么sendAddressMap占用的內(nèi)容會(huì)越來越大, 直到產(chǎn)生OutOfMemoryError異常. 下面我們?cè)偬砑哟a定時(shí)清理過期的數(shù)據(jù).
4、清理過期數(shù)據(jù)
FrequencyFilter.java
/** * 在上面代碼的基礎(chǔ)上, 再添加如下代碼 */ public class FrequencyFilter implements SmsFilter { private long cleanMapInterval; private Timer timer = new Timer("sms_frequency_filter_clear_data_thread"); @Override public void init() { timer.schedule(new TimerTask() { @Override public void run() { cleanSendAddressMap(); } }, cleanMapInterval, cleanMapInterval); } /** * 將sendAddressMap中的所有過期數(shù)據(jù)刪除 */ private void cleanSendAddressMap() { long currentTime = System.currentTimeMillis(); long expireSendTime = currentTime - sendInterval; for(String key : sendAddressMap.keySet()) { Long sendTime = sendAddressMap.get(key); if(sendTime < expireSendTime) { sendAddressMap.remove(key, sendTime); } } } @Override public void destroy() { timer.cancel(); } }
這段程序不算復(fù)雜, 啟動(dòng)一個(gè)定時(shí)器, 每隔cleanMapInterval毫秒執(zhí)行一次cleanSendAddressMap方法清理過期數(shù)據(jù).
cleanSendAddressMap方法中首先獲取當(dāng)前時(shí)間, 根據(jù)當(dāng)前時(shí)間獲得一個(gè)時(shí)間值: 所有在這個(gè)時(shí)間之后發(fā)送短信的, 現(xiàn)在不可以再次發(fā)送短信. 然后從整個(gè)map中刪除所有value小于這個(gè)時(shí)間值的鍵值對(duì).
當(dāng)然, 添加上面的代碼后, 最開始的代碼又有bug了: 當(dāng)最后一行sendAddressMap.replace(id, sendTime, currentTime)執(zhí)行失敗時(shí)不一定是其他線程進(jìn)行了替換, 也有可能是清理線程把數(shù)據(jù)刪了. 所以我們需要修改setSendTime方法最后幾行:
FrequencyFilter.java
private boolean setSendTime(String id) { // 省略前面的代碼 if(sendAddressMap.replace(id, sendTime, currentTime)) { return true; } return sendAddressMap.putIfAbsent(id, currentTime) == null; }
這里如果替換成功, 那么直接返回true.
如果替換不成功. 那么可能是其他線程先替換了(第一種情況); 也可能是被清理線程刪除了(第二種情況); 甚至可以能是先被清理線程刪除了, 又有其他線程插入了新的時(shí)間值(第三種情況).
- 如果是第一種情況 或者 第三種情況, 那么情況和最開始分析的一樣, 可以直接認(rèn)為不能發(fā)送.
- 如果是第二種情況, 那么應(yīng)該是可以發(fā)送的.
- 為了確認(rèn)是哪種情況, 我們可以執(zhí)行一次putIfAbsent, 如果成功, 說明是第二種情況, 可以發(fā)送; 否則是第一種或者第三種情況, 不能發(fā)送.
至此, 限制發(fā)送時(shí)間的代碼就算是完成了. 當(dāng)然, 這段程序還有一個(gè)小bug或者說"特性":
假如, IP為"192.168.0.1"的客戶請(qǐng)求向手機(jī)號(hào)"12345678900"發(fā)送短信, 然后在sendInterval之內(nèi)又在IP為"192.168.0.2"的機(jī)器上請(qǐng)求向手機(jī)號(hào)"12345678900"發(fā)送短信. 那么短信將不會(huì)發(fā)出去, 而且手機(jī)號(hào)"12345678900"的上次發(fā)送時(shí)間被置為當(dāng)前時(shí)間.
5、使用實(shí)例
下面我們提供一個(gè)Server層, 展示如何將上一篇以及這一篇中的代碼整合到一起:
SmsService.java
public class SmsService{ private Sms sms; private List<SmsFilter> filters; private Properties template; // 省略了部分代碼 /** * 發(fā)送驗(yàn)證碼 * * @param smsEntity 發(fā)送短信的基本數(shù)據(jù) * @return 如果提交成功, 返回0. 否則返回其他值. */ public int sendCaptcha(SmsEntity smsEntity){ for(SmsFilter filter : filters) { if(!filter.filter(smsEntity)){ return 1; } } if(SmsEntity.REGISTER_TYPE.equals(smsEntity.getType())) { sendRegisterSms(smsEntity); } else{ return 2; } return 0; } /** * 發(fā)送注冊(cè)驗(yàn)證碼 * * @param smsEntity 發(fā)送短信的基本數(shù)據(jù) */ private void sendRegisterSms(SmsEntity smsEntity) { sms.sendMessage(smsEntity.getMobile(), template.getProperty("register").replace("{captcha}", smsEntity.getCaptcha())); } }
之后將FrequencyFilter以及上一篇中的AsyncSmsImpl通過set方法"注入"進(jìn)去即可。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家學(xué)習(xí)java程序設(shè)計(jì)有所幫助。
相關(guān)文章
Java遞歸算法經(jīng)典實(shí)例(經(jīng)典兔子問題)
本文主要對(duì)經(jīng)典的兔子案例分析,來進(jìn)一步更好的理解和學(xué)習(xí)java遞歸算法,具有很好的參考價(jià)值,需要的朋友一起來看下吧2016-12-12java基礎(chǔ)之Collection與Collections和Array與Arrays的區(qū)別
這篇文章主要介紹了java基礎(chǔ)之Collection與Collections和Array與Arrays的區(qū)別的相關(guān)資料,本文主要說明兩者的區(qū)別以防大家混淆概念,需要的朋友可以參考下2017-08-08SpringBoot整合EasyExcel?3.x的完整示例
EasyExcel 是一個(gè)基于 Java 的、快速、簡(jiǎn)潔、解決大文件內(nèi)存溢出的 Excel 處理工具,它能讓你在不用考慮性能、內(nèi)存的等因素的情況下,快速完成 Excel 的讀、寫等功能,這篇文章主要介紹了SpringBoot整合EasyExcel3.x的過程,需要的朋友可以參考下2023-07-07Spring強(qiáng)大事務(wù)兼容數(shù)據(jù)庫(kù)多種組合解決業(yè)務(wù)需求
這篇文章主要為大家介紹了Spring強(qiáng)大事務(wù)兼容數(shù)據(jù)庫(kù)多種組合解決業(yè)務(wù)需求示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07