1、数据表结构
因为需要记录整天的发送记录,所以在此把数据保存到数据库中. 数据表结构如下图所示:
type为验证码的类型,例如注册、重置密码等,sendTime的默认值为当前时间。
2、限制日发送次数
在此需要使用到接口和实体类:
DailyCountFilter.java
public class DailyCountFilter implements SmsFilter { private int ipDailyMaxSendCount; private int mobileDailyMaxSendCount; private SmsDao smsDao; //省略了部分无用的代码 @Override public boolean filter(SmsEntity smsEntity) { if (smsDao.getMobileCount(smsEntity.getMobile()) >= mobileDailyMaxSendCount) { return false; } if (smsDao.getIPCount(smsEntity.getIp()) >= ipDailyMaxSendCount) { return false; } smsDao.saveEntity(smsEntity); return true; } }
主要代码非常的简单,首先判断向指定的手机号发送的次数是否达到了日最大发送次数,接着再判断指定的ip请求发送的次数是否达到了最大次数,要是都没有达到, 则把本次发送的手机号、ip等信息保存到数据库中。
当然这个类存在一些问题:在判断是否超过最大次数到保存实体数据之间可能已有其他线程保存了新的数据,造成上面的两个判断并不是绝对的准确。
可使用序列化等级的事务保证不会发生错误,不过代价非常的高,所以在此不做处理,因为前面已经实现了限制发送频率,要是先使用FrequencyFilter过滤一次,限制发送频率的话,那基本上无法出现前面说的问题。
另外还有个问题:随着时间的推移,这一个表将会越来越大,造成查询的性能会非常的的差,可以每隔一段时间就删除无用的数据;也可以动态创建表,之后向新表中插入数据。
3、使用动态表
在后此使用第二种方案:数据表的名字为"sms_四位年_两位月",例如"sms_2016_02"。插入数据的时候根据现在的时间获得表名,接着再插入。此外使用Quartz在每月的20号2点生成下个月和下下个月的数据表:
首先修改DailyCountFilter类,在这一个类中添加任务计划,定时生成数据表:
DailyCountFilter.java
// 在上面代码基础上,再添加如下的代码: public class DailyCountFilter implements SmsFilter { private Scheduler sched; @Override public void init() throws SchedulerException { smsDao.createTable(0); // 创建这一个月的数据表 smsDao.createTable(1); // 创建下一个月的数据表 SchedulerFactory sf = new StdSchedulerFactory(); sched = sf.getScheduler(); // 创建Quartz容器 JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("smsDao", smsDao); // 创建运行任务时需要使用的数据map // 创建job对象, 该对象执行实际的任务 JobDetail job = JobBuilder.newJob(CreateSmsTableJob.class) .usingJobData(jobDataMap) .withIdentity("create sms table job").build(); // 创建trigger对象, 该对象用来描述触发执行job的时间规则 // 比如这里的每月20号2点 CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("create sms table trigger") .withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 20 * ?"))// 每月的20号2点 .build(); sched.scheduleJob(job, trigger); // 注册任务和触发规则 sched.start(); // 启动调度 } @Override public void destroy() { try { sched.shutdown(); } catch (SchedulerException e) {} } public static class CreateSmsTableJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap dataMap = context.getJobDetail().getJobDataMap(); SmsDao smsDao = (SmsDao) dataMap.get("smsDao"); // 获得传过来的smsDao对象 smsDao.createTable(1); // 创建下个月的数据表 smsDao.createTable(2); // 创建下下个月的数据表 } } }
接着来看一看SmsDao的部分代码:
SmsDao.java
public class SmsDao { /** * 创建新日志表 * * @param monthExcursion 偏移的月数 */ public void createTable(int monthExcursion){ String sql = "CREATE TABLE IF NOT EXISTS " + getTableName(monthExcursion) + " LIKE sms"; // 执行sql语句 } /** * 保存SmsEntity实体对象 */ public void saveEntity(SmsEntity smsEntity){ String sql = "INSERT INTO " + getNowTableName() + " (mobile, ip, type) VALUES(?, ?, ?)"; // 执行sql语句 } /** * 获得指定手机号码今天请求发送短信的次数 * * @param mobile 用户手机号码 * @return 今天请求发送短信的次数 */ public long getMobileCount(String mobile){ String sql = "SELECT count(id) FROM " + getNowTableName() + " WHERE mobile=? AND time >= CURDATE()"; // 执行sql语句, 返回查询结果 } // 省略了getIPCount方法 /** * 获得现在使用的表的名字 */ private String getNowTableName() { return getTableName(0); } private DateFormat dateFormat = new SimpleDateFormat("yyyy_MM"); /** * 获得相对现在偏移monthExcursion月的表名 * * @param monthExcursion 偏移的月数 * @return 对应月的表名 */ private String getTableName(int monthExcursion) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.MONTH, monthExcursion); Date date = calendar.getTime(); return "sms_" + dateFormat.format(date); } }
SmsDao中的createTable方法成功运行有一个前提,那就是存在sms数据表。createTable方法会复制sms表的结构创建新的数据表。
在保留发送短信的数据(手机号码、ip以及时间等),而不是直接删除,是由于之后可能需要分析这一些数据,获取我们想要的信息,例如判断服务商短信的到达率、是否有人恶意发送短信等,甚至可能会获得意外的"惊喜"哦。