Redis原子性写入HASH结构数据并设置过期时间
Redis中提供了原子性命令SETEX或SET来写入STRING类型数据并设置Key的过期时间:
> SET key value EX NX
ok
> SETEX key value
ok
但对于HASH结构则没有这样的命令,只能先写入数据然后设置过期时间:
> HSET key field value
ok
> EXPIRE key 60
ok
这样就带了一个问题:HSET命令执行成功而EXPIRE命令执行失败(如命令未能成功发送到Redis服务器),那么数据将不会过期。针对这个问题,本文提供了几种解决方案:
Lua脚本
向Redis中写入HASH结构的Lua脚本如下:
local fieldIndex=
local valueIndex=
local key=KEYS[]
local fieldCount=ARGV[]
local expired=ARGV[]
for i=,fieldCount, do
redis.pcall('HSET',key,ARGV[fieldIndex],ARGV[valueIndex])
fieldIndex=fieldIndex+
valueIndex=valueIndex+
end
redis.pcall('EXPIRE',key,expired)
使用Redis命令行工具执行Lua脚本,需要将脚本内容单行化,并以分号间隔不同的命令:
> SCRIPT LOAD "local fieldIndex=3;local valueIndex=4;local key=KEYS[1];local fieldCount=ARGV[1];local expired=ARGV[2];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);"
"e03e7868920b7669d1c8c8b16dcee86ebfac650d"
> evalsha e03e7868920b7669d1c8c8b16dcee86ebfac650d key 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 * + ];
redisValues[] = fieldCount;
redisValues[] = seconds;
var i = ;
foreach (var item in valueDict)
{
redisValues[i] = item.Key;
redisValues[i + ] = item.Value;
i += ;
}
//await Database.ScriptEvaluateAsync(script, new RedisKey[] { key, fieldCount.ToString(), seconds.ToString() }, redisValues);
await Database.ScriptEvaluateAsync(script, new RedisKey[] { key }, redisValues);
} await ExecuteCommandAsync(func, $"redisError:hashWrite:{key}");
}
事务
Redis官方文档在事务一节中指出:Redis命令只会在有语法错误或对Key使用了错误的数据类型时执行失败。因此,只要我们保证将正确的写数据和设置过期时间的命令作为一个整体发送到服务器端即可,使用Lua脚本正式基于此。
StackExchange.Redis官方文档中关于事务的说明,参见:Transactions
以下是代码实现:
public async Task<bool> WriteAsync(string key, IDictionary<string, string> valueDict, TimeSpan expiry)
{
var tranc = Database.CreateTransaction();
foreach (var item in valueDict)
{
tranc.HashSetAsync(key, item.Key, item.Value);
}
tranc.KeyExpireAsync(key, expiry);
return await tranc.ExecuteAsync();
}
占位符
这种方案比较差,思路如下,共分为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原子性写入HASH结构数据并设置过期时间的更多相关文章
- 使用redis事物解决stringRedisTemplate.setIfAbsent()并设置过期时间遇到的问题
spring-date-redis版本:1.6.2场景:在使用setIfAbsent(key,value)时,想对key设置一个过期时间,同时需要用到setIfAbsent的返回值来指定之后的流程,所 ...
- 如何为Redis中list中的项设置过期时间
问题 两种解决方法 有序集合 多个集合以及TTL Redis是一个伟大的工具,用来在内存中存储列表是很合适的. 不过,如果你想要快速搜索列表,同时需要让列表中每项都在一定时间后过期,应该怎么做呢? 首 ...
- redis hash结构如何设置过期时间
Redis中有个设置时间过期的功能,即通过setex或者expire实现,目前redis没有提供hsetex()这样的方法,redis中过期时间只针对顶级key类型,对于hash类型是不支持的,这个时 ...
- redis数据库如何批量删除键和设置过期时间?
我们可以借助Linux中的xargs,在终端中执行命令来实现这两个功能. 一.批量删除键 批量删除以"key"开头key的方法,需要借助Linux中的xargs,在终端中执行以下命 ...
- redis中的key设置过期时间
EXPIRE key seconds 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除. 在 Redis 中,带有生存时间的 key 被称为『易失的 ...
- java操作Redis缓存设置过期时间
关于Redis的概念和应用本文就不再详解了,说一下怎么在java应用中设置过期时间. 在应用中我们会需要使用redis设置过期时间,比如单点登录中我们需要随机生成一个token作为key,将用户的信息 ...
- redis文档翻译_key设置过期时间
Available since 1.0.0. 使用開始版本号1.01 Time complexity: O(1) 时间复杂度O(1) 出处:http://blog.csdn.net/colum ...
- redis 一二事 - 设置过期时间,以文件夹形式展示key显示缓存数据
在使用redis时,有时回存在大量数据的时候,而且分类相同,ID相同 可以使用hset来设置,这样有一个大类和一个小分类和一个value组成 但是hset不能设置过期时间 过期时间只能在set上设置 ...
- redis批量设置过期时间
Redis 中有删除单个 Key 的指令 DEL,但好像没有批量删除 Key 的指令,不过我们可以借助 Linux 的 xargs 指令来完成这个动作.代码如下: redis-cli keys &qu ...
随机推荐
- SQL Server 2008收缩日志文件--dbcc shrinkfile参数说明
原文:SQL Server 2008收缩日志文件--dbcc shrinkfile参数说明 DBCC SHRINKFILE 收缩相关数据库的指定数据文件或日志文件大小. 语法 DBCC SHRINKF ...
- 8086 CPU 寄存器简介(超详细,图文并茂)
http://www.cnblogs.com/BoyXiao/archive/2010/11/20/1882716.html
- FC红白机游戏列表(维基百科)
1055个fc游戏列表 日文名 中文译名 英文版名 发行日期 发行商 ドンキーコング 大金刚 Donkey Kong 1983年7月15日 任天堂 ドンキーコングJR. 大金刚Jr. Donkey K ...
- Android零基础入门第81节:Activity数据传递
在Android开发中,经常要在Activity之间传递数据.前面也学习了Activity和Intent相关基础,接下来一起来学习Activity的数据传递. 一.简介 通过前面的学习知道,Inten ...
- 给 Web 开发人员推荐的通用独立 UI 组件(二)
现代 Web 开发在将体验和功能做到极致的同时,对于美观的追求也越来越高.在推荐完图形库之后,再来推荐一些精品的独立 UI 组件.这些组件可组合在一起,形成美观而交互强大的 Web UI . 给 We ...
- x64系统的判断和x64下文件和注册表访问的重定向(举例了GetProcAddress后转成函数指针的用法)
判断一个32位应用程序是否运行在x64系统下,可以使用下面的代码: BOOL IsX64System() { BOOL bIsWow64 = FALSE; typedef BOOL (WINAPI * ...
- asp.net core 系列之Response caching 之 Distributed caching(3)
这篇文章讲解分布式缓存,即 Distributed caching in ASP.NET Core Distributed caching in ASP.NET Core 分布式缓存是可以在多个应用服 ...
- 如何使用jQuery可以让滚轮滚到底部可以自动加载所需内容
话不多说先上代码 $(window).scroll(function() { var scrollTop = $(this).scrollTop(); //滚动高度 var windowHeig ...
- Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe
目录: Java NIO 学习笔记(一)----概述,Channel/Buffer Java NIO 学习笔记(二)----聚集和分散,通道到通道 Java NIO 学习笔记(三)----Select ...
- Mint-ui全局引入
1.Mint-ui在全局引入之后,在组件中使用其中的js函数,如toast() this.$toast('在全局引入之后可以直接在this.$toast中使用')