承接上篇同步/非同步實作,本文實作「同一手機號/IP 每日發送上限」管控。採用資料庫記錄發送日誌,搭配 Quartz 定時任務按月分表(sms_yyyy_MM),避免單表過大導致查詢效能下降,並提供完整 DAO 與 Filter 實作代碼。
CREATE TABLE sms (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
mobile VARCHAR(20), ip VARCHAR(45), type TINYINT, -- type: 1=註冊, 2=重置密碼
send_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
public class DailyCountFilter implements SmsFilter {
private int ipDailyMax, mobileDailyMax;
private SmsDao dao;
@Override
public boolean filter(SmsEntity entity) {
if (dao.getMobileCount(entity.getMobile()) >= mobileDailyMax) return false;
if (dao.getIPCount(entity.getIp()) >= ipDailyMax) return false;
dao.saveEntity(entity); // 記錄本次發送
return true;
}
}
sms_2024_01、sms_2024_02...CREATE TABLE ... LIKE sms)。getNowTableName() 根據當前日期決定操作哪張表。public class SmsDao {
private final SimpleDateFormat fmt = new SimpleDateFormat("yyyy_MM");
String getTableName(int monthOffset) {
Calendar c = Calendar.getInstance();
c.add(Calendar.MONTH, monthOffset);
return "sms_" + fmt.format(c.getTime());
}
void createTable(int offset) {
jdbc.execute("CREATE TABLE IF NOT EXISTS " + getTableName(offset) + " LIKE sms");
}
void saveEntity(SmsEntity e) {
jdbc.update("INSERT INTO " + getTableName(0) + " (mobile, ip, type) VALUES (?,?,?)",
e.getMobile(), e.getIp(), e.getType());
}
long getMobileCount(String mobile) {
return jdbc.queryForObject(
"SELECT COUNT(*) FROM " + getTableName(0) + " WHERE mobile=? AND send_time >= CURDATE()",
Long.class, mobile);
}
// getIPCount 同理
}
「檢查計數 → 寫入日誌」非原子操作,極高併發下可能超賣。實務上可加分散式鎖(Redis SETNX)或資料庫唯一索引配合重試,本文為教學示範暫不處理,因前置頻率限制已能攔截大部分突發流量。