Redis有效時(shí)間設(shè)置以及時(shí)間過期處理操作
本文對(duì)redis的過期處理機(jī)制做個(gè)簡單的概述,讓大家有個(gè)基本的認(rèn)識(shí)。
Redis中有個(gè)設(shè)置時(shí)間過期的功能,即對(duì)存儲(chǔ)在redis數(shù)據(jù)庫中的值可以設(shè)置一個(gè)過期時(shí)間。作為一個(gè)緩存數(shù)據(jù)庫,這是非常實(shí)用的。如我們一般項(xiàng)目中的token或者一些登錄信息,尤其是短信驗(yàn)證碼都是有時(shí)間限制的,按照傳統(tǒng)的數(shù)據(jù)庫處理方式,一般都是自己判斷過期,這樣無疑會(huì)嚴(yán)重影響項(xiàng)目性能。
一、有效時(shí)間設(shè)置:
redis對(duì)存儲(chǔ)值的過期處理實(shí)際上是針對(duì)該值的鍵(key)處理的,即時(shí)間的設(shè)置也是設(shè)置key的有效時(shí)間。Expires字典保存了所有鍵的過期時(shí)間,Expires也被稱為過期字段。
四種處理策略
EXPIRE 將key的生存時(shí)間設(shè)置為ttl秒
PEXPIRE 將key的生成時(shí)間設(shè)置為ttl毫秒
EXPIREAT 將key的過期時(shí)間設(shè)置為timestamp所代表的的秒數(shù)的時(shí)間戳
PEXPIREAT 將key的過期時(shí)間設(shè)置為timestamp所代表的的毫秒數(shù)的時(shí)間戳
其實(shí)以上幾種處理方式都是根據(jù)PEXPIREAT來實(shí)現(xiàn)的,設(shè)置生存時(shí)間的時(shí)候是redis內(nèi)部計(jì)算好時(shí)間之后在內(nèi)存處理的,最終的處理都會(huì)轉(zhuǎn)向PEXPIREAT。
1、2兩種方式是設(shè)置一個(gè)過期的時(shí)間段,就是咱們處理驗(yàn)證碼最常用的策略,設(shè)置三分鐘或五分鐘后失效,把分鐘數(shù)轉(zhuǎn)換成秒或毫秒存儲(chǔ)到redis中。
3、4兩種方式是指定一個(gè)過期的時(shí)間 ,比如優(yōu)惠券的過期時(shí)間是某年某月某日,只是單位不一樣。
二、過期處理
過期鍵的處理就是把過期鍵刪除,這里的操作主要是針對(duì)過期字段處理的。
Redis中有三種處理策略:定時(shí)刪除、惰性刪除和定期刪除。
定時(shí)刪除:在設(shè)置鍵的過期時(shí)間的時(shí)候創(chuàng)建一個(gè)定時(shí)器,當(dāng)過期時(shí)間到的時(shí)候立馬執(zhí)行刪除操作。不過這種處理方式是即時(shí)的,不管這個(gè)時(shí)間內(nèi)有多少過期鍵,不管服務(wù)器現(xiàn)在的運(yùn)行狀況,都會(huì)立馬執(zhí)行,所以對(duì)CPU不是很友好。
惰性刪除:惰性刪除策略不會(huì)在鍵過期的時(shí)候立馬刪除,而是當(dāng)外部指令獲取這個(gè)鍵的時(shí)候才會(huì)主動(dòng)刪除。處理過程為:接收get執(zhí)行、判斷是否過期(這里按過期判斷)、執(zhí)行刪除操作、返回nil(空)。
定期刪除:定期刪除是設(shè)置一個(gè)時(shí)間間隔,每個(gè)時(shí)間段都會(huì)檢測是否有過期鍵,如果有執(zhí)行刪除操作。這個(gè)概念應(yīng)該很好理解。
看完上面三種策略后可以得出以下結(jié)論:
4. 1、3為主動(dòng)刪除,2為被動(dòng)刪除。
5. 1是實(shí)時(shí)執(zhí)行的,對(duì)CPU不是很友好,但是這在最大程度上釋放了內(nèi)存,所以這種方式算是一種內(nèi)存優(yōu)先優(yōu)化策略。
6. 2、3為被動(dòng)刪除,所以過期鍵應(yīng)該會(huì)存在一定的時(shí)間,這樣就使得過期鍵不會(huì)被立馬刪除,仍然占用著內(nèi)存。但是惰性刪除的時(shí)候一般是單個(gè)刪除,相對(duì)來說對(duì)CPU是友好的。
7. 定期鍵這種刪除策略是一種讓人很蛋疼的策略,它既有避免1、2兩種策略劣勢的可能,也有同時(shí)發(fā)生1、2兩種策略劣勢的可能。如果定期刪除執(zhí)行的過于頻繁就可能會(huì)演變成定時(shí)刪除,如果執(zhí)行的過少就有可能造成過多過期鍵未被刪除而占用過多內(nèi)存,如果時(shí)間的設(shè)置不是太好,既可能占用過多內(nèi)存又同時(shí)對(duì)CPU產(chǎn)生不好的影響。所以。使用定期刪除的時(shí)候一定要把握好這個(gè)刪除的時(shí)間點(diǎn)。存在即為合理,既然開發(fā)的時(shí)候有這種策略,就說明定期刪除還是有他的優(yōu)勢的,具體大家可以自己琢磨。
三、主從服務(wù)器刪除過期鍵處理
參考書上說的有三種:RDB持久化、AOF持久化和復(fù)制功能。
RDB:
1. 主服務(wù)器模式運(yùn)行在載入RDB文件時(shí),程序會(huì)檢查文件中的鍵,只會(huì)加載未過期的,過期的會(huì)被忽略,所以RDB模式下過期鍵不會(huì)對(duì)主服務(wù)器產(chǎn)生影響。
2. 從服務(wù)器運(yùn)行載入RDB文件時(shí),會(huì)載入所有鍵,包括過期和未過期。當(dāng)主服務(wù)器進(jìn)行數(shù)據(jù)同步的時(shí)候,從服務(wù)器的數(shù)據(jù)會(huì)被清空,所以RDB文件的過期鍵一般不會(huì)對(duì)從服務(wù)器產(chǎn)生影響。
AOF:
AOF文件不會(huì)受過期鍵的影響。如果有過期鍵未被刪除,會(huì)執(zhí)行以下動(dòng)作:
客戶端請(qǐng)求時(shí)(過期鍵):
1.從數(shù)據(jù)庫充刪除被訪問的過期鍵;
2.追加一條DEL 命令到AOF文件;
3.向執(zhí)行請(qǐng)求的客戶端回復(fù)nil(空)。
復(fù)制:
1.主服務(wù)器刪除過期鍵之后,向從服務(wù)器發(fā)送一條DEL指令,告知?jiǎng)h除該過期鍵。
2.從服務(wù)器接收到get指令的時(shí)候不會(huì)對(duì)過期鍵進(jìn)行處理,只會(huì)當(dāng)做未過期鍵一樣返回。(為了保持主從服務(wù)器數(shù)據(jù)的一致性)
3.從服務(wù)器只有接到主服務(wù)器發(fā)送的DEL指令后才會(huì)刪除過期鍵。
參考書籍:《Redis設(shè)計(jì)與實(shí)現(xiàn)》黃健宏著
補(bǔ)充知識(shí):redis緩存數(shù)據(jù)需要指定緩存有效時(shí)間范圍段的多個(gè)解決方案 Calendar+quartz
在實(shí)現(xiàn)積分項(xiàng)目業(yè)務(wù)中,對(duì)不同場景設(shè)置了不同的key-value緩存到了redis中。但是因?yàn)閷?duì)不同業(yè)務(wù)的key需要緩存的時(shí)間不盡相同,這里自定義工具類來實(shí)現(xiàn)。
設(shè)置redis緩存key,截取部分代碼:
try{
//cacheManager就相當(dāng)從redis鏈接池獲取一個(gè)連接,具體工廠類獲取在后面?zhèn)渥?
cacheManager = (RedisCacheManager) CacheManagerFactory.getCacheManager();
totalMonCount = Float.parseFloat(cacheManager.getString(monthKey)) + centCount;
if (centLimitByMonth != -1){
if (totalMonCount > centLimitByMonth) {
// 超出月上限不再累計(jì)
logger.error("exceeds the month limit cents! [" + totalMonCount + "] code:[" + code + "]");
return null;
}
}
//當(dāng)未超出月額度,此時(shí)要對(duì)日額度進(jìn)行判斷;只需判斷其是否超出日上限積分
if (dayKey != null){
//累積積分;因?yàn)楹灥狡鋵?shí)是沒有每日積分的,是按次數(shù)規(guī)則累積的;但為了統(tǒng)一,直接用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());
}
//對(duì)月限制key進(jìn)行積分累加
//每月1號(hào)凌晨1點(diǎn)啟動(dòng)腳本刪除,同時(shí)設(shè)置了保存到月底緩存時(shí)間雙重保障
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 連接池實(shí)例
*
* @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服務(wù)信息: 當(dāng)前連接的服務(wù)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"); // 獲取當(dāng)前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()); 就用到了工具類獲取了指定的時(shí)間范圍。
對(duì)于redis這種希望指定緩存有效時(shí)間,現(xiàn)在提供3種方案:
1、自定義確切時(shí)間:
public static final long LoginCentTimeByDay = 86400;//s 未認(rèn)證失效時(shí)間 1天
public static final long LoginCentTimeByMonth = 86400*30; //s 時(shí)效時(shí)間 30天
直接指定:
cacheManager.set(monthKey, totalMonCount.toString(),LoginCentTimeByDay)
2、自定義工具類,獲取當(dāng)前時(shí)間到第二天的零點(diǎn)、下個(gè)月1號(hào)零點(diǎn)的時(shí)間差(s):
cacheManager.set(monthKey, totalMonCount.toString(), DateUtil.getSecsToEndOfCurrentDay());
public class DateUtil {
/**
*獲取每月最后一天時(shí)間
* @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;
}
/*
獲取下一個(gè)月第一天凌晨1點(diǎn)
*/
public static Date nextMonthFirstDate() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 1); //設(shè)置為每月凌晨1點(diǎn)
calendar.set(Calendar.DAY_OF_MONTH, 1); //設(shè)置為每月1號(hào)
calendar.add(Calendar.MONTH, 1); // 月份加一,得到下個(gè)月的一號(hào)
// calendar.add(Calendar.DATE, -1); 下一個(gè)月減一為本月最后一天
return calendar.getTime();
}
/**
* 獲取第二天凌晨0點(diǎn)毫秒數(shù)
* @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();
}
//*********
/**
* 獲取當(dāng)前時(shí)間到下個(gè)月凌晨1點(diǎn)相差秒數(shù)
* @return
*/
public static Long getSecsToEndOfCurrentMonth(){
Long secsOfNextMonth = nextMonthFirstDate().getTime();
//將當(dāng)前時(shí)間轉(zhuǎn)為毫秒數(shù)
Long secsOfCurrentTime = new Date().getTime();
//將時(shí)間轉(zhuǎn)為秒數(shù)
Long distance = (secsOfNextMonth - secsOfCurrentTime)/1000;
if (distance > 0 && distance != null){
return distance;
}
return new Long(0);
}
/**
* 獲取當(dāng)前時(shí)間到明天凌晨0點(diǎn)相差秒數(shù)
* @return
*/
public static Long getSecsToEndOfCurrentDay() throws ParseException {
Long secsOfNextDay = nextDayFirstDate().getTime();
//將當(dāng)前時(shí)間轉(zhuǎn)為毫秒數(shù)
Long secsOfCurrentTime = new Date().getTime();
//將時(shí)間轉(zhuǎn)為秒數(shù)
Long distance = (secsOfNextDay - secsOfCurrentTime)/1000;
if (distance > 0 && distance != null){
return distance;
}
return new Long(0);
}
}
3、使用定時(shí)任務(wù)定時(shí)清空redis緩存;避免出現(xiàn)定時(shí)任務(wù)異常,我的業(yè)務(wù)代碼里都保障了兩種方案都適用。
定時(shí)任務(wù)保證,到指定時(shí)間直接調(diào)用代碼進(jìn)行操作;代碼里直接調(diào)用shell腳本直接刪掉相關(guān)redis的緩存數(shù)據(jù)。
quartz定時(shí)任務(wù)就需要注意定義相應(yīng)的cron時(shí)間:
我的定時(shí)任務(wù)的配置文件quartz.xml中定義:
<!--定時(shí)任務(wù)1-->
<!-- 每天12點(diǎn)將當(dāng)天用戶積分行為緩存清掉 -->
<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" /><!-- 定時(shí)執(zhí)行 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 點(diǎn)執(zhí)行 -->
<!--<property name="cronExpression" value="0 */1 * * * ?" /><!– 每隔1min執(zhí)行一次 –>-->
</bean>
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="deleteRedisCacheDayUsersTrigger" />
<ref bean="deleteRedisCacheMonthUsersTrigger" />
<!--暫時(shí)不用-->
<!--<ref bean="centUpdateByMonthTrigger" />-->
</list>
</property>
</bean>
以上這篇Redis有效時(shí)間設(shè)置以及時(shí)間過期處理操作就是小編分享給大家的全部內(nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決maven打包排除類不生效maven-compiler-plugin問題
總結(jié):在Spring Boot項(xiàng)目B中作為項(xiàng)目A的依賴時(shí),排除啟動(dòng)類不生效的原因是被其他類引用或父POM引入,解決方法是跳過test編譯或注釋掉@SpringBootTest(classes={BApplication.class})2024-11-11
SpringBoot框架實(shí)現(xiàn)切換啟動(dòng)開發(fā)環(huán)境和測試環(huán)境
這篇文章主要介紹了SpringBoot框架實(shí)現(xiàn)切換啟動(dòng)開發(fā)環(huán)境和測試環(huán)境,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
SpringBoot結(jié)合Vue實(shí)現(xiàn)投票系統(tǒng)過程詳解
這篇文章主要介紹了SpringBoot+Vue框架實(shí)現(xiàn)投票功能的項(xiàng)目系統(tǒng),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09
SpringBoot創(chuàng)建JSP登錄頁面功能實(shí)例代碼
這篇文章主要介紹了SpringBoot創(chuàng)建JSP登錄頁面功能實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-04-04

