Redis原子性写入HASH结构数据并设置过期时间

Redis中提供了原子性命令SETEX或SET来写入STRING类型数据并设置Key的过期时间:

> SET key value EX 60 NX
ok
> SETEX key 60 value
ok

但对于HASH结构则没有这样的命令,只能先写入数据然后设置过期时间:

> HSET key field value
ok
> EXPIRE key 60
ok

这样就带了一个问题:HSET命令执行成功而EXPIRE命令执行失败(如命令未能成功发送到Redis服务器),那么数据将不会过期。针对这个问题,本文提供两种解决方案:

Lua脚本

向Redis中写入HASH结构的Lua脚本如下:

local fieldIndex=1
local valueIndex=2
local key=KEYS[1]
local fieldCount=KEYS[2]
local expired=KEYS[3]
for i=1,fieldCount,1 do
  redis.pcall(‘HSET‘,key,ARGV[fieldIndex],ARGV[valueIndex])
  fieldIndex=fieldIndex+2
  valueIndex=valueIndex+2
end
redis.pcall(‘EXPIRE‘,key,expired)

使用Redis命令行工具执行Lua脚本,需要将脚本内容单行化,并以分号间隔不同的命令:

>  SCRIPT LOAD "local fieldIndex=1;local valueIndex=2;local key=KEYS[1];local fieldCount=KEYS[2];local expired=KEYS[3];for i=1,fieldCount,1 do redis.pcall(‘HSET‘,key,ARGV[fieldIndex],ARGV[valueIndex]) fieldIndex=fieldIndex+2 valueIndex=valueIndex+2 end;redis.pcall(‘EXPIRE‘,key,expired);"
"00bfad4f66e549fc57df9cc5f98022c34ada3ef1"
> EVALSHA 00bfad4f66e549fc57df9cc5f98022c34ada3ef1 3 key 2 60 field1 value1 field2 value2
nil

写入结果:

使用StackExchange.Redis执行Lua脚本:

public async Task WriteAsync(string key, IDictionary<string, string> valueDict, TimeSpan expiry)
{
    async Task func()
    {
        if (valueDict.Empty())
        {
            return;
        }
        // 可以将脚本内容缓存下来以避免多起读取脚本文件
        var luaScriptPath = $"{AppDomain.CurrentDomain.BaseDirectory}/Lua/HSET.lua";
        var script = File.ReadAllText(luaScriptPath);
        var seconds = (int)Math.Ceiling(expiry.TotalSeconds);
        var fieldCount = valueDict.Count;
        var redisValues = new RedisValue[fieldCount * 2];
        var i = 0;
        foreach (var item in valueDict)
        {
            redisValues[i] = item.Key;
            redisValues[i + 1] = item.Value;
            i += 2;
        }
        await Database.ScriptEvaluateAsync(script, new RedisKey[] { key, fieldCount.ToString(), seconds.ToString() }, redisValues);
    }

    await ExecuteCommandAsync(func, $"redisError:hashWrite:{key}");
}

占位符

思路如下,共分为4步,每一步都有可能失败:

  • 先写入一个特殊的值,如Nil表示无数据
  • 若第一步操作成功,则Key被写入Redis。然后对Key设置过期时间。若第一步失败,则Key未写入Redis,设置过期时间会失败
  • 若成功设置Key的过期时间则像Redis中写入有效数据
  • 删除第一步中设置的特殊值

在读取Hash的值时,判断读到的field的值是否是Nil,若是则删除并忽略,若不是则处理。

代码如下:

namespace RedisClient.Imples
{
    public class RedisHashOperator : RedisCommandExecutor, IRedisHashOperator
    {
        private readonly string KeyExpiryPlaceHolder = "expiryPlaceHolder";

        public RedisHashOperator(ILogger<RedisHashOperator> logger, IRedisConnection redisConnection)
            : base(logger, redisConnection)
        {
        }

        public async Task WriteAsync(string key, IDictionary<string, string> valueDict, TimeSpan expiry)
        {
            async Task action()
            {
                if (valueDict.Empty())
                {
                    return;
                }
                var hashList = new List<HashEntry>();
                foreach (var value in valueDict)
                {
                    hashList.Add(new HashEntry(value.Key, value.Value));
                }
                await Database.HashSetAsync(key, hashList.ToArray());
            }

            async Task successed()
            {
                await ExecuteCommandAsync(action, $"redisEorror:hashWrite:{key}");
            }

            await SetKeyExpireAsync(key, expiry, successed);
        }

        public async Task<RedisReadResult<IDictionary<string, string>>> ReadAllFieldsAsync(string key)
        {
            async Task<RedisReadResult<IDictionary<string, string>>> func()
            {
                var redisReadResult = new RedisReadResult<IDictionary<string, string>>();
                if (Database.KeyExists(key) == false)
                {
                    return redisReadResult.Failed();
                }
                var resultList = await Database.HashGetAllAsync(key);
                if (resultList == null)
                {
                    return redisReadResult.Failed();
                }
                var dict = new Dictionary<string, string>();
                if (resultList.Any())
                {
                    foreach (var result in resultList)
                    {
                        if (result.Name == KeyExpiryPlaceHolder || result.Value == KeyExpiryPlaceHolder)
                        {
                            await RemoveKeyExpiryPlaceHolderAsync(key);
                            continue;
                        }
                        dict[result.Name] = result.Value;
                    }
                }
                return redisReadResult.Success(dict);
            }

            return await ExecuteCommandAsync(func, $"redisError:hashReadAll:{key}");
        }

        #region private
        /// <summary>
        /// 设置HASH结构KEY的过期时间
        /// </summary>
        /// <param name="successed">设置过期时间成功之后的回调函数</param>
        private async Task SetKeyExpireAsync(string key, TimeSpan expiry, Func<Task> successed)
        {
            // 确保KEY的过期时间写入成功之后再执其它的操作
            await Database.HashSetAsync(key, new HashEntry[] { new HashEntry(KeyExpiryPlaceHolder, KeyExpiryPlaceHolder) });
            if (Database.KeyExpire(key, expiry))
            {
                await successed();
            }
            await Database.HashDeleteAsync(key, KeyExpiryPlaceHolder);
        }

        private async Task RemoveKeyExpiryPlaceHolderAsync(string key)
        {
            await Database.HashDeleteAsync(key, KeyExpiryPlaceHolder);
        }
        #endregion

    }
}

文中多次出现的ExecuteCommandAsync方法主要目的是实现针对异常情况的统一处理,实现如下:

namespace RedisClient.Imples
{
    public class RedisCommandExecutor
    {
        private readonly ILogger Logger;
        protected readonly IDatabase Database;

        public RedisCommandExecutor(ILogger<RedisCommandExecutor> logger, IRedisConnection redisConnection)
        {
            Logger = logger;
            Database = redisConnection.GetDatabase();
        }

        protected async Task ExecuteCommandAsync(Func<Task> func, string errorMessage = null)
        {
            try
            {
                await func();
            }
            catch (Exception ex)
            {
                if (string.IsNullOrEmpty(errorMessage))
                {
                    errorMessage = ex.Message;
                }
                Logger.LogError(errorMessage, ex);
            }
        }

        protected async Task<T> ExecuteCommandAsync<T>(Func<Task<T>> func, string errorMessage = null)
        {
            try
            {
                return await func();
            }
            catch (Exception ex)
            {
                if (string.IsNullOrEmpty(errorMessage))
                {
                    errorMessage = ex.Message;
                }
                Logger.LogError(errorMessage, ex);
                return default(T);
            }
        }
    }
}

小结

Redis官方文档在事务一节中指出:Redis命令只会在有语法错误或对Key使用了错误的数据类型时执行失败。因此,只要我们保证将正确的写数据和设置过期时间的命令作为一个整体发送到服务器端即可,使用Lua脚本正式基于此。

除了上面提到的两种方式之外,还可以使用Redis中的事务来解决这个问题。

原文地址:https://www.cnblogs.com/Cwj-XFH/p/11216074.html

时间: 2024-10-10 01:25:38

Redis原子性写入HASH结构数据并设置过期时间的相关文章

redis hash结构如何设置过期时间

Redis中有个设置时间过期的功能,即通过setex或者expire实现,目前redis没有提供hsetex()这样的方法,redis中过期时间只针对顶级key类型,对于hash类型是不支持的,这个时候,我们可以采用,所以如果想对hash进行expires设置,可以采用下面的方法: redis 127.0.0.1:6379> hset expire:me name tom (integer) 0 redis 127.0.0.1:6379> hget expire:me name "t

redis文档翻译_key设置过期时间

Available since 1.0.0.    使用开始版本1.01 Time complexity: O(1)  时间复杂度O(1) 出处:http://blog.csdn.net/column/details/redisbanli.html Set a timeout on key. After the timeout has expired, the key will automatically be deleted. A key with an associated timeout

java操作Redis缓存设置过期时间

关于Redis的概念和应用本文就不再详解了,说一下怎么在java应用中设置过期时间. 在应用中我们会需要使用redis设置过期时间,比如单点登录中我们需要随机生成一个token作为key,将用户的信息转为json串作为value保存在redis中,通常做法是: //生成token String token = UUID.randomUUID().toString(); //把用户信息写入redis jedisClient.set(REDIS_USER_SESSION_KEY + ":"

redis 一二事 - 设置过期时间,以文件夹形式展示key显示缓存数据

在使用redis时,有时回存在大量数据的时候,而且分类相同,ID相同 可以使用hset来设置,这样有一个大类和一个小分类和一个value组成 但是hset不能设置过期时间 过期时间只能在set上设置 1 // 向redis中添加缓存 2 jedisClient.set(REDIS_ITEM_KEY + ":" + itemId + ":" + ITEM_KEY, JsonUtils.objectToJson(item)); 3 // 设置key的过期时间 4 jed

laravel redis存数组并设置过期时间

$data = [ 'zoneList'=>$zoneList, 'eqList' => $eqList, 'mdateList' => $mdateList, 'workhoursList' => $workhoursList, 'pricerangeList' => $pricerangeList, ]; Redis::set($cacheKey, serialize($data)); Redis::expire($cacheKey, 300); laravel门面set

redis 设置过期时间

1.设置过期时间功能:即对存储在 redis 数据库中的值可以设置一个过期时间.作为一个缓存数据库,这是非常实用的.如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能.我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间. 2.redis删除过期key策略:假设你设置了一批 key 只能存活1个小时,那么接下

localStorage二次封装-----设置过期时间

export default{ set(key,data,time){ let obj={ data=data, ctime:(new Date()).getTime(),//时间戳,同Date.now() express:1000*60*60//设置过期时间一个小时 } localStorage.setItem(key,JSON.stringify(obj)); }, get(key){ let obj=JSON.parse(localStorage.getItem(key)); let ge

redis key设置过期时间

最近做的一个项目需要用到redis存储storm计算的结果,使用过程中发现我的redis使用内存空间一直在增大,颇为好奇,因为我都设置了key的过期时间了呀.. 最后一看代码才发现问题.原来我都是在代码中先调用expire()方法调用顺序有问题. expire(key,time) 如果当前redis没有这个key的时候默认是不操作的.哎,写代码千万得严谨啊

关于jQuery的cookies插件2.2.0版设置过期时间的说明

欢迎转载,转载请注明作者RunningOn jQuery应该是各位用JavaScript做web开发的常用工具了,它有些插件能非常方便地操作cookie. 不过非常让人郁闷的是,网上几乎所有人对于这些插件所做的关于cookie过期/失效时间的说明都是含混的或不正确的.我被这玩意搞得实在不行了,去看了其中两个插件的源代码终于明白了是怎么一回事.为避免更多人中招,我就写下我RunningOn个人对这些cookie插件的理解. 首先要说明的是cookie插件国内主要流行的有两个,一个是早在2006年的