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 ...
随机推荐
- C#通过HttpListener实现HTTP监听
代码: using NLog; using System; using System.Diagnostics; using System.IO; using System.Net; using Sys ...
- WPF使用Fluent.Ribbon修改标题栏背景颜色
使用NuGet安装:Install-Package Fluent.Ribbon 修改App.xaml: <Application.Resources> <!-- Attach def ...
- Android零基础入门第29节:善用TableLayout表格布局,事半功倍
原文:Android零基础入门第29节:善用TableLayout表格布局,事半功倍 前面学习了线性布局和相对布局,线性布局虽然方便,但如果遇到控件需要排列整齐的情况就很难达到要求,用相对布局又比较麻 ...
- 网络文件系统nfs文件系统使用(很全面)
一.NFS简介 1.NFS就是Network FileSystem的缩写,它的最大功能就是可以通过网络让不同的机器,不同的操作系统彼此共享文件(sharefiles)——可以通过NFS挂载远程主机的目 ...
- Ext5.1日期控件仅显示年月
1.注册xtype类型 2.保存文件为xxxx.js 3.使用 xtype : monthfield return this.buildToolbar({ items: [ { xtype: 'mon ...
- 论文阅读计划2(Deep Joint Rain Detection and Removal from a Single Image)
Deep Joint Rain Detection and Removal from a Single Image[1] 简介:多任务全卷积从单张图片中去除雨迹.本文在现有的模型上,开发了一种多任务深 ...
- AspNetCore 小记
1. Microsoft.AspNetCore.Hosting.IHostingEnvironment 的接口获取的值: WebRootPath:D:\参考资料\C#\AspNetCore开源项目\n ...
- 智能合约开发——以太坊 DApp 实现 购买通证token
合约的buy()方法用于提供购买股票的接口.注意关键字payable,有了它买股票的人才可以付钱给你. 接收钱没有比这个再简单的了! function buy() payable public ret ...
- 微软Skype实时口译增加中文
直击现场 在机器翻译技术上,微软的 Skype 业务也算是行业内名列前茅.日前其实时口语翻译技术再次跃升一个台阶,新增了对中文(普通话)的翻译支持. 据美国科技新闻网站 TheVerge 报道,此前, ...
- ASP.NET Web API Controller 是怎么建成的
先看ASP.NET Web API 讯息管线: 註:为了避免图片太大以至于超过版面,上图中的「HTTP 讯息处理程序」区块省略了 HttpRoutingDispatcher 处理路由分派的部分.「控制 ...