1. 程式人生 > >redis資料庫佇列(list),集合(set)元素設定類似過期(expire)功能

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。
redis util分塊

待解決的問題:
blockSize過大,會導致應過期資料堆積,當blockSize = expire時,redis最多需要額外儲存100%的資料。如果redis空間緊張,應該適當減小blockSize
blockSize過小,會導致redis讀取次數增多,redis讀取平均增加 expire/(blockSize*2) 次,如果redis訪問太慢應該適當增加blockSize

blockSize很小時可以考慮用multi加快訪問速度

Hash存取同理