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结构数据并设置过期时间的更多相关文章

  1. 使用redis事物解决stringRedisTemplate.setIfAbsent()并设置过期时间遇到的问题

    spring-date-redis版本:1.6.2场景:在使用setIfAbsent(key,value)时,想对key设置一个过期时间,同时需要用到setIfAbsent的返回值来指定之后的流程,所 ...

  2. 如何为Redis中list中的项设置过期时间

    问题 两种解决方法 有序集合 多个集合以及TTL Redis是一个伟大的工具,用来在内存中存储列表是很合适的. 不过,如果你想要快速搜索列表,同时需要让列表中每项都在一定时间后过期,应该怎么做呢? 首 ...

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

    Redis中有个设置时间过期的功能,即通过setex或者expire实现,目前redis没有提供hsetex()这样的方法,redis中过期时间只针对顶级key类型,对于hash类型是不支持的,这个时 ...

  4. redis数据库如何批量删除键和设置过期时间?

    我们可以借助Linux中的xargs,在终端中执行命令来实现这两个功能. 一.批量删除键 批量删除以"key"开头key的方法,需要借助Linux中的xargs,在终端中执行以下命 ...

  5. redis中的key设置过期时间

    EXPIRE key seconds 为给定  key  设置生存时间,当  key  过期时(生存时间为  0  ),它会被自动删除. 在 Redis 中,带有生存时间的  key  被称为『易失的 ...

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

    关于Redis的概念和应用本文就不再详解了,说一下怎么在java应用中设置过期时间. 在应用中我们会需要使用redis设置过期时间,比如单点登录中我们需要随机生成一个token作为key,将用户的信息 ...

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

    Available since 1.0.0.    使用開始版本号1.01 Time complexity: O(1)  时间复杂度O(1) 出处:http://blog.csdn.net/colum ...

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

    在使用redis时,有时回存在大量数据的时候,而且分类相同,ID相同 可以使用hset来设置,这样有一个大类和一个小分类和一个value组成 但是hset不能设置过期时间 过期时间只能在set上设置 ...

  9. redis批量设置过期时间

    Redis 中有删除单个 Key 的指令 DEL,但好像没有批量删除 Key 的指令,不过我们可以借助 Linux 的 xargs 指令来完成这个动作.代码如下: redis-cli keys &qu ...

随机推荐

  1. Visual studio 创建通用项目失败vstemplate

    Visual studio 创建项目失败 提示 the vstemplate file references the wizard class 'Microsoft.VisualStudio.WinR ...

  2. Android零基础入门第66节:RecyclerView点击事件处理

    前面两期学习了RecyclerView的简单使用,并为其item添加了分割线.在实际运用中,无论是List还是Grid效果,基本都会伴随着一些点击操作,那么本期就来一起学习RecyclerView的点 ...

  3. 80%的岗位是没有太多能力上的要求的(少部分聪明的人开始觉醒,这部分一定是那些主动追求、主动学习的人;30岁现象能区分真正专业和不学无术的人)good

    不要沦陷程序员的30岁问题     热门> 就是学习能力和工作热情态度的问题. 我之前也跟作者一样思考过这个问题,答案是否定的. 在知识积累的行业,年纪越大,越吃香,比如金融,医学,IT.就怕3 ...

  4. Qt5---ftp上传功能(使用组合的办法实现功能,QNetworkAccessManager自动管理分片上传,用QLoggingCategory屏蔽SSL警告)

      从Qt的版本进化中可以知道,在Qt4中的QFtp类到了Qt5中已经没有了,虽然可以通过在Qt5中自己编译出QFtp,但是Qt5中提供的QNetworkAccessManager在发送和请求网络方面 ...

  5. delphi hook alt+F4 ctrl+delete+alt win键等

    unit uHook; interface uses  Windows, Messages, SysUtils, Variants, Classes, Controls, Forms, Dialogs ...

  6. Codility--- NumberOfDiscIntersections

    Task description We draw N discs on a plane. The discs are numbered from 0 to N − 1. A zero-indexed ...

  7. 一文详解 LVS、Nginx 及 HAProxy 工作原理( 附大图 )

    当前大多数的互联网系统都使用了服务器集群技术,集群是将相同服务部署在多台服务器上构成一个集群整体对外提供服务,这些集群可以是 Web 应用服务器集群,也可以是数据库服务器集群,还可以是分布式缓存服务器 ...

  8. 点菜网---Java开源生鲜电商平台-技术选型(源码可下载)

    点菜网---Java开源生鲜电商平台-技术选型(源码可下载) 1.内容简介 点菜网目前选用的是最流行的微服务架构模式,采用前后端分离的开发模式,具备高可用,高负载,支持千万级别的数据量的请求. 2. ...

  9. HBase 学习之路(六)——HBase Java API 的基本使用

    一.简述 截至到目前(2019.04),HBase 有两个主要的版本,分别是1.x 和 2.x ,两个版本的Java API有所不同,1.x 中某些方法在2.x中被标识为@deprecated过时.所 ...

  10. 08、MySQL—字符串型

    字符串型 1.Char 定长字符:指定长度之后,系统一定会分配指定的空间用于存储数据 基本语法: char(L),L代表字符数(中文与英文字母一样),L长度为0到255 2.Varchar 变长字符: ...