本文对redis的过期处理机制做个简单的概述,让大家有个基本的认识。
redis中有个设置时间过期的功能,即对存储在redis数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的token或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
一、有效时间设置:
redis对存储值的过期处理实际上是针对该值的键(key)处理的,即时间的设置也是设置key的有效时间。expires字典保存了所有键的过期时间,expires也被称为过期字段。
四种处理策略
expire 将key的生存时间设置为ttl秒
pexpire 将key的生成时间设置为ttl毫秒
expireat 将key的过期时间设置为timestamp所代表的的秒数的时间戳
pexpireat 将key的过期时间设置为timestamp所代表的的毫秒数的时间戳
其实以上几种处理方式都是根据pexpireat来实现的,设置生存时间的时候是redis内部计算好时间之后在内存处理的,最终的处理都会转向pexpireat。
1、2两种方式是设置一个过期的时间段,就是咱们处理验证码最常用的策略,设置三分钟或五分钟后失效,把分钟数转换成秒或毫秒存储到redis中。
3、4两种方式是指定一个过期的时间 ,比如优惠券的过期时间是某年某月某日,只是单位不一样。
二、过期处理
过期键的处理就是把过期键删除,这里的操作主要是针对过期字段处理的。
redis中有三种处理策略:定时删除、惰性删除和定期删除。
定时删除:在设置键的过期时间的时候创建一个定时器,当过期时间到的时候立马执行删除操作。不过这种处理方式是即时的,不管这个时间内有多少过期键,不管服务器现在的运行状况,都会立马执行,所以对cpu不是很友好。
惰性删除:惰性删除策略不会在键过期的时候立马删除,而是当外部指令获取这个键的时候才会主动删除。处理过程为:接收get执行、判断是否过期(这里按过期判断)、执行删除操作、返回nil(空)。
定期删除:定期删除是设置一个时间间隔,每个时间段都会检测是否有过期键,如果有执行删除操作。这个概念应该很好理解。
看完上面三种策略后可以得出以下结论:
4. 1、3为主动删除,2为被动删除。
5. 1是实时执行的,对cpu不是很友好,但是这在最大程度上释放了内存,所以这种方式算是一种内存优先优化策略。
6. 2、3为被动删除,所以过期键应该会存在一定的时间,这样就使得过期键不会被立马删除,仍然占用着内存。但是惰性删除的时候一般是单个删除,相对来说对cpu是友好的。
7. 定期键这种删除策略是一种让人很蛋疼的策略,它既有避免1、2两种策略劣势的可能,也有同时发生1、2两种策略劣势的可能。如果定期删除执行的过于频繁就可能会演变成定时删除,如果执行的过少就有可能造成过多过期键未被删除而占用过多内存,如果时间的设置不是太好,既可能占用过多内存又同时对cpu产生不好的影响。所以。使用定期删除的时候一定要把握好这个删除的时间点。存在即为合理,既然开发的时候有这种策略,就说明定期删除还是有他的优势的,具体大家可以自己琢磨。
三、主从服务器删除过期键处理
参考书上说的有三种:rdb持久化、aof持久化和复制功能。
rdb:
1. 主服务器模式运行在载入rdb文件时,程序会检查文件中的键,只会加载未过期的,过期的会被忽略,所以rdb模式下过期键不会对主服务器产生影响。
2. 从服务器运行载入rdb文件时,会载入所有键,包括过期和未过期。当主服务器进行数据同步的时候,从服务器的数据会被清空,所以rdb文件的过期键一般不会对从服务器产生影响。
aof:
aof文件不会受过期键的影响。如果有过期键未被删除,会执行以下动作:
客户端请求时(过期键):
1.从数据库充删除被访问的过期键;
2.追加一条del 命令到aof文件;
3.向执行请求的客户端回复nil(空)。
复制:
1.主服务器删除过期键之后,向从服务器发送一条del指令,告知删除该过期键。
2.从服务器接收到get指令的时候不会对过期键进行处理,只会当做未过期键一样返回。(为了保持主从服务器数据的一致性)
3.从服务器只有接到主服务器发送的del指令后才会删除过期键。
参考书籍:《redis设计与实现》黄健宏著
补充知识:redis缓存数据需要指定缓存有效时间范围段的多个解决方案 calendar+quartz
在实现积分项目业务中,对不同场景设置了不同的key-value缓存到了redis中。但是因为对不同业务的key需要缓存的时间不尽相同,这里自定义工具类来实现。
设置redis缓存key,截取部分代码:
try{
//cachemanager就相当从redis链接池获取一个连接,具体工厂类获取在后面备注
cachemanager = (rediscachemanager) cachemanagerfactory.getcachemanager();
totalmoncount = float.parsefloat(cachemanager.getstring(monthkey)) + centcount;
if (centlimitbymonth != -1){
if (totalmoncount > centlimitbymonth) {
// 超出月上限不再累计
logger.error("exceeds the month limit cents! [" + totalmoncount + "] code:[" + code + "]");
return null;
}
}
//当未超出月额度,此时要对日额度进行判断;只需判断其是否超出日上限积分
if (daykey != null){
//累积积分;因为签到其实是没有每日积分的,是按次数规则累积的;但为了统一,直接用centcount代替(都是签一次1分)
totaldaycount = float.parsefloat(cachemanager.getstring(daykey)) + centcount;
if (centlimitbyday != -1){
if (totaldaycount > centlimitbyday){
logger.info("[error]teacher everyday assign cents > month limit! total: ["+totaldaycount+"]");
return null;
}
}
cachemanager.set(daykey,totaldaycount.tostring(),dateutil.getsecstoendofcurrentday());
}
//对月限制key进行积分累加
//每月1号凌晨1点启动脚本删除,同时设置了保存到月底缓存时间双重保障
cachemanager.set(monthkey, totalmoncount.tostring(), dateutil.getsecstoendofcurrentday());
logger.info("==monthkey:"+monthkey+"---value:"+totalmoncount);
}
...
}catch(exception e){
logger.error("===cache redis fail!");
e.printstacktrace();
}finally {
if (cachemanager != null){
cachemanager.close();
}
}
//工厂类获取redis链接
public class cachemanagerfactory {
private static icachemanager cachemanager;
private cachemanagerfactory(){
};
public static icachemanager getcachemanager(){
if(cachemanager == null){
synchronized (cachemanagerfactory.class) {
if(cachemanager == null){
jedispooler jedispooler = redispoolerfactory.getjedispooler();
cachemanager = new rediscachemanager(jedispooler);
}
}
}
return cachemanager;
}
}
//redis链接池工厂类获取链接
public class redispoolerfactory {
private static jedispooler jedispooler;
private redispoolerfactory(){
};
public static jedispooler getjedispooler(){
if(jedispooler == null){
synchronized (redispoolerfactory.class) {
if(jedispooler == null){
jedispooler = new jedispooler();
}
}
}
return jedispooler;
}
}
/**
*
* redis 连接池实例
*
* @author ethan.lam
* @createtime 2011-12-3
*
*/
public class jedispooler {
private jedispool pool;
private string redis_host;
private string redis_psw;
private int redis_port;
private int redis_maxactive;
private int redis_maxidle;
private int redis_maxwait;
public jedispooler(string config) {
__init(config);
}
public jedispooler() {
__init("/jedispool.properties");
}
private void __init(string conf) {
// 完成初始化工作
properties prop = new properties();
try {
inputstream _file = loadconfig(conf);
prop.load(_file);
redis_host = prop.getproperty("redis.host");
redis_psw = prop.getproperty("redis.psw");
redis_port = integer.parseint(prop.getproperty("redis.port").trim());
redis_maxactive = integer.parseint(prop.getproperty("redis.maxactive").trim());
redis_maxidle = integer.parseint(prop.getproperty("redis.maxidle").trim());
redis_maxwait = integer.parseint(prop.getproperty("redis.maxwait").trim());
} catch (exception e) {
e.printstacktrace();
redis_host = "localhost";
throw new nullpointerexception(conf + " is not found !");
}
jedispoolconfig config = new jedispoolconfig();
config.setmaxactive(redis_maxactive);
config.setmaxidle(redis_maxidle);
config.setmaxwait(redis_maxwait);
config.settestonborrow(true);
system.out.println("redis cache服务信息: 当前连接的服务ip为:" + redis_host + ":" + redis_port);
if (null == redis_psw || "".equals(redis_psw.trim())){
pool = new jedispool(config, redis_host, redis_port, 5000);
}
else{
pool = new jedispool(config, redis_host, redis_port, 5000, redis_psw);
}
}
public jedis getjedis() {
return pool.getresource();
}
public void returnresource(jedis jedis) {
pool.returnresource(jedis);
}
public jedispool getpool() {
return pool;
}
inputstream loadconfig(string configpath) throws exception {
inputstream _file = null;
try {
string file = jedispooler.class.getresource(configpath).getfile();
file = urldecoder.decode(file);
_file = new fileinputstream(file);
} catch (exception e) {
system.out.println("读取jar中的配置文件....");
string currentjarpath = urldecoder.decode(jedispooler.class.getprotectiondomain()
.getcodesource().getlocation().getfile(), "utf-8"); // 获取当前jar文件名
system.out.println("currentjarpath:" + currentjarpath);
java.util.jar.jarfile currentjar = new java.util.jar.jarfile(currentjarpath);
java.util.jar.jarentry dbentry = currentjar.getjarentry("jedispool.properties");
inputstream in = currentjar.getinputstream(dbentry);
_file = in;
}
return _file;
}
}
可以看到,这里cachemanager.set(monthkey, totalmoncount.tostring(), dateutil.getsecstoendofcurrentday()); 就用到了工具类获取了指定的时间范围。
对于redis这种希望指定缓存有效时间,现在提供3种方案:
1、自定义确切时间:
public static final long logincenttimebyday = 86400;//s 未认证失效时间 1天
public static final long logincenttimebymonth = 86400*30; //s 时效时间 30天
直接指定:
cachemanager.set(monthkey, totalmoncount.tostring(),logincenttimebyday)
2、自定义工具类,获取当前时间到第二天的零点、下个月1号零点的时间差(s):
cachemanager.set(monthkey, totalmoncount.tostring(), dateutil.getsecstoendofcurrentday());
public class dateutil {
/**
*获取每月最后一天时间
* @param sdate1
* @return
*/
public static date getlastdayofmonth(date sdate1) {
calendar cday1 = calendar.getinstance();
cday1.settime(sdate1);
final int lastday = cday1.getactualmaximum(calendar.day_of_month);
date lastdate = cday1.gettime();
lastdate.setdate(lastday);
return lastdate;
}
/*
获取下一个月第一天凌晨1点
*/
public static date nextmonthfirstdate() {
calendar calendar = calendar.getinstance();
calendar.set(calendar.hour_of_day, 1); //设置为每月凌晨1点
calendar.set(calendar.day_of_month, 1); //设置为每月1号
calendar.add(calendar.month, 1); // 月份加一,得到下个月的一号
// calendar.add(calendar.date, -1); 下一个月减一为本月最后一天
return calendar.gettime();
}
/**
* 获取第二天凌晨0点毫秒数
* @return
*/
public static date nextdayfirstdate() throws parseexception {
calendar cal = calendar.getinstance();
cal.settime(new date());
cal.add(calendar.day_of_year, 1);
cal.set(calendar.hour_of_day, 00);
cal.set(calendar.minute, 0);
cal.set(calendar.second, 0);
return cal.gettime();
}
//*********
/**
* 获取当前时间到下个月凌晨1点相差秒数
* @return
*/
public static long getsecstoendofcurrentmonth(){
long secsofnextmonth = nextmonthfirstdate().gettime();
//将当前时间转为毫秒数
long secsofcurrenttime = new date().gettime();
//将时间转为秒数
long distance = (secsofnextmonth - secsofcurrenttime)/1000;
if (distance > 0 && distance != null){
return distance;
}
return new long(0);
}
/**
* 获取当前时间到明天凌晨0点相差秒数
* @return
*/
public static long getsecstoendofcurrentday() throws parseexception {
long secsofnextday = nextdayfirstdate().gettime();
//将当前时间转为毫秒数
long secsofcurrenttime = new date().gettime();
//将时间转为秒数
long distance = (secsofnextday - secsofcurrenttime)/1000;
if (distance > 0 && distance != null){
return distance;
}
return new long(0);
}
}
3、使用定时任务定时清空redis缓存;避免出现定时任务异常,我的业务代码里都保障了两种方案都适用。
定时任务保证,到指定时间直接调用代码进行操作;代码里直接调用shell脚本直接删掉相关redis的缓存数据。
quartz定时任务就需要注意定义相应的cron时间:
我的定时任务的配置文件quartz.xml中定义:
<!--定时任务1-->
<!-- 每天12点将当天用户积分行为缓存清掉 -->
<bean id="deleterediscachedayusersjob" class="cn.qtone.xxt.cent.quartz.delrediscachecentusers" />
<bean id="deleterediscachedayuserstask" class="org.springframework.scheduling.quartz.methodinvokingjobdetailfactorybean">
<property name="targetobject" ref="deleterediscachedayusersjob" />
<property name="targetmethod" value="delcurrentdaycacheusersbyday" /><!-- 定时执行 doitem 方法 -->
<property name="concurrent" value="false" />
</bean>
<bean id="deleterediscachedayuserstrigger" class="org.springframework.scheduling.quartz.crontriggerbean">
<property name="jobdetail" ref="deleterediscachedayuserstask" />
<property name="cronexpression" value="59 59 23 * * ?" /><!-- 每天凌晨23:59:59 点执行 -->
<!--<property name="cronexpression" value="0 */1 * * * ?" /><!– 每隔1min执行一次 –>-->
</bean>
<bean class="org.springframework.scheduling.quartz.schedulerfactorybean">
<property name="triggers">
<list>
<ref bean="deleterediscachedayuserstrigger" />
<ref bean="deleterediscachemonthuserstrigger" />
<!--暂时不用-->
<!--<ref bean="centupdatebymonthtrigger" />-->
</list>
</property>
</bean>
以上这篇redis有效时间设置以及时间过期处理操作就是www.887551.com分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持www.887551.com。