redis資料庫佇列(list),集合(set)元素設定類似過期(expire)功能
**(2018-07-23更新:
本案例使用的解決方案以及程式碼,在較大資料量的操作情境下存在嚴重redis效能問題——大集合的一次性刪除操作可能導致redis阻塞,正常業務將無法訪問、操作redis。如果需要使用相關功能,請使用資料中的1方法或自行優化redis刪除佇列操作的執行時間)**
問題:
專案需要為每個使用者維護一個列表,存放一些資料。列表中的值有過期時間,過期的值查詢可以找到也可以找不到,還會有一個驗證,所以無所謂。但是redis佇列只有一個整體的過期功能,沒有每個元素的單獨過期功能,所以如果使用者一直不停向佇列塞東西,佇列就會變的越來越大。這顯然不合理。
資料:
查了一下資料,目前給佇列、集合元素單獨設定過期不可能做到(redis4.0.2)。但是有其他方法可以做到類似功能。
參考的一篇文章提出兩種方法:
1.使用SortedSet,使用score引數代表unix時間,程式定期使用ZRANGEBYSCORE清除過期項
2.將集合拆分成多個按時間排序、自動過期的小集合
1方法顯然更方便、高效,但是專案必須跑程式為每個集合定期維護,可能產生很多不必要的麻煩,所以我選擇2解決方法。
解決方案:
(重要的話說三遍:這個解決方案中過期的值有可能被返回,過期的值有可能被返回,過期的值有可能被返回。如果需要返回確定不過期的值,請在value中加unix時間作驗證)
使用一個工具類封裝redis操作,自動進行redis集合的拆分和查詢。直接上程式碼:
必須獲得redis讀、寫、設定過期、查詢key是否存在 函式
expire表示過期時間(秒); blockSize表示分塊大小(秒),不能大於expire
package com.study.javaweb.test1.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* User: longjuanfeng Date: 2017-11-29
*/
public class RedisAutoExpireUtils<ValueType, SetResponse> {
private Logger logger = LoggerFactory.getLogger(RedisAutoExpireUtils.class);
private RedisSetter<ValueType, SetResponse> redisSetter;
private RedisGetter<ValueType> redisGetter;
private RedisExpire redisExpire;
private RedisExists redisExists;
private Integer expire;
//default size is expire
private Integer blockSize;
public RedisAutoExpireUtils(RedisSetter<ValueType, SetResponse> redisSetter, RedisGetter<ValueType> redisGetter, RedisExpire redisExpire, RedisExists redisExists, Integer expire) throws Exception {
this(redisSetter, redisGetter, redisExpire, redisExists, expire, expire);
}
public RedisAutoExpireUtils(RedisSetter<ValueType, SetResponse> redisSetter, RedisGetter<ValueType> redisGetter, RedisExpire redisExpire, RedisExists redisExists, Integer expire, Integer blockSize) throws Exception {
if (blockSize > expire) {
throw new Exception("blockSize should not larger than expire");
}
this.redisSetter = redisSetter;
this.redisGetter = redisGetter;
this.redisExpire = redisExpire;
this.redisExists = redisExists;
this.expire = expire;
this.blockSize = blockSize;
}
public SetResponse setRedisValue(String key, ValueType value) {
Integer nowTime = new Long(System.currentTimeMillis() / 1000).intValue();
Integer blockTail = nowTime % blockSize;
Integer stampPos = nowTime - blockTail;
String timeStamp = String.valueOf(stampPos);
logger.info("timeStamp of {} is {}", nowTime, timeStamp);
String keyWithStamp = key + ":" + timeStamp;
Boolean exists = redisExists.exists(keyWithStamp) == 1;
SetResponse result = redisSetter.addValue(keyWithStamp, value);
if (!exists) {
Integer expireTime = expire + blockSize - blockTail;
redisExpire.setExpire(keyWithStamp, expireTime);
logger.info("set expire of {} is {}", nowTime, expireTime);
}
return result;
}
public ValueType getRedisValue(String key) {
Integer nowTime = new Long(System.currentTimeMillis() / 1000).intValue();
Integer checkBlockNum = expire / blockSize + 1;
Integer blockTail = nowTime % blockSize;
Integer stampPos = nowTime - blockTail;
for (int i = 0; i < checkBlockNum; i++) {
String timeStamp = String.valueOf(stampPos);
logger.info("check timeStamp of {} is {}", nowTime, timeStamp);
String keyWithStamp = key + ":" + timeStamp;
ValueType value = redisGetter.getValue(keyWithStamp);
if (value != null) {
logger.info("find value at timeStamp of {}", timeStamp);
return value;
}
stampPos = stampPos - blockSize;
}
return null;
}
@FunctionalInterface
public interface RedisSetter<ValueType, SetResponse> {
SetResponse addValue(String key, ValueType value);
}
@FunctionalInterface
public interface RedisGetter<ValueType> {
ValueType getValue(String key);
}
@FunctionalInterface
public interface RedisExpire {
void setExpire(String key, Integer expire);
}
@FunctionalInterface
public interface RedisExists {
Integer exists(String key);
}
}
使用方法
redisTestRepository是一個讀寫redis功能類
package com.study.javaweb.test1.repository;
import com.study.javaweb.test1.BaseTest;
import com.study.javaweb.test1.utils.RedisAutoExpireUtils;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* User: longjuanfeng Date: 2017-11-28
*/
public class RedisTest extends BaseTest {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedisTestRepository redisTestRepository;
@Test
public void redisAutoExpireUtilsTest1() throws Exception {
Integer fixedExpireTime = 20;
String testKey = "redisUtilTest1";
RedisAutoExpireUtils<String, Long> redisAutoExpireUtils = new RedisAutoExpireUtils<>(redisTestRepository::setAdd, redisTestRepository::setGet, redisTestRepository::setExpire, key -> redisTestRepository.checkExists(key) ? 1 : 0, fixedExpireTime, fixedExpireTime / 3);
redisAutoExpireUtils.setRedisValue(testKey, "12233");
redisAutoExpireUtils.setRedisValue(testKey, "12234");
Thread.sleep(10000);
redisAutoExpireUtils.setRedisValue(testKey, "12235");
redisAutoExpireUtils.setRedisValue(testKey, "12236");
String result = redisAutoExpireUtils.getRedisValue(testKey);
logger.info("get pop data of {} : {}", testKey, result);
Thread.sleep(5000);
result = redisAutoExpireUtils.getRedisValue(testKey);
logger.info("get pop data of {} : {}", testKey, result);
result = redisAutoExpireUtils.getRedisValue(testKey);
logger.info("get pop data of {} : {}", testKey, result);
Thread.sleep(7000);
result = redisAutoExpireUtils.getRedisValue(testKey);
logger.info("get pop data of {} : {}", testKey, result);
}
}
執行結果
[18:24:10:294] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.setRedisValue(RedisAutoExpireUtils.java:70) - timeStamp of 1511951050 is 1511951046
[18:24:10:883] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.setRedisValue(RedisAutoExpireUtils.java:77) - set expire of 1511951050 is 22
[18:24:10:884] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.setRedisValue(RedisAutoExpireUtils.java:70) - timeStamp of 1511951050 is 1511951046
[18:24:20:886] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.setRedisValue(RedisAutoExpireUtils.java:70) - timeStamp of 1511951060 is 1511951058
[18:24:20:887] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.setRedisValue(RedisAutoExpireUtils.java:77) - set expire of 1511951060 is 24
[18:24:20:889] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.setRedisValue(RedisAutoExpireUtils.java:70) - timeStamp of 1511951060 is 1511951058
[18:24:20:890] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951060 is 1511951058
[18:24:20:894] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:93) - find value at timeStamp of 1511951058
[18:24:20:896] [INFO] - com.study.javaweb.test1.repository.RedisTest.redisAutoExpireUtilsTest1(RedisTest.java:36) - get pop data of redisUtilTest1 : 12236
[18:24:25:897] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951065 is 1511951064
[18:24:25:899] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951065 is 1511951058
[18:24:25:901] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:93) - find value at timeStamp of 1511951058
[18:24:25:903] [INFO] - com.study.javaweb.test1.repository.RedisTest.redisAutoExpireUtilsTest1(RedisTest.java:44) - get pop data of redisUtilTest1 : 12235
[18:24:25:904] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951065 is 1511951064
[18:24:25:907] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951065 is 1511951058
[18:24:25:909] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951065 is 1511951052
[18:24:25:910] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951065 is 1511951046
[18:24:25:911] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:93) - find value at timeStamp of 1511951046
[18:24:25:912] [INFO] - com.study.javaweb.test1.repository.RedisTest.redisAutoExpireUtilsTest1(RedisTest.java:46) - get pop data of redisUtilTest1 : 12234
[18:24:32:913] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951072 is 1511951070
[18:24:32:914] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951072 is 1511951064
[18:24:32:915] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951072 is 1511951058
[18:24:32:916] [INFO] - com.study.javaweb.test1.utils.RedisAutoExpireUtils.getRedisValue(RedisAutoExpireUtils.java:89) - check timeStamp of 1511951072 is 1511951052
[18:24:32:917] [INFO] - com.study.javaweb.test1.repository.RedisTest.redisAutoExpireUtilsTest1(RedisTest.java:54) - get pop data of redisUtilTest1 : null
與預期一致
設計思路:
將整個時間線切分成block,用每個block頭的time stamp作為redis的key,單獨設定過期。查詢時,查多個block。
待解決的問題:
blockSize過大,會導致應過期資料堆積,當blockSize = expire時,redis最多需要額外儲存100%的資料。如果redis空間緊張,應該適當減小blockSize
blockSize過小,會導致redis讀取次數增多,redis讀取平均增加 expire/(blockSize*2) 次,如果redis訪問太慢應該適當增加blockSize
blockSize很小時可以考慮用multi加快訪問速度
Hash存取同理