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. 用JavaScriptSerializer解析JSON

    引用System.Web.Extensions using System.Web.Script.Serialization; var serializer = new JavaScriptSerial ...

  2. UWP入门(四)--设置控件样式

    原文:UWP入门(四)--设置控件样式 官方定义:可以使用 XAML 框架通过多种方式自定义应用的外观. 通过样式可以设置控件属性,并重复使用这些设置,以便保持多个控件具有一致的外观. 可分享至不同e ...

  3. MIPS开发板的“不二”选择——Creator Ci20单板计算机评测(芯片是君正JZ4780 ,也就是MIPS R3000,系统推荐Debian或深度,官网就有,其它语言有FreePascal和Go和Java和Python)

    在MIPS架构的CPU上开发软件,当然需要使用MIPS专用的工具链来编译代码.不过一般的LINUX发行版内都有相应的配套工具链供用户使用.Ci20出厂时的LINUX发行版为DEBIAN 7.5,相应的 ...

  4. Android零基础入门第86节:探究Fragment生命周期

    一个Activity可以同时组合多个Fragment,一个Fragment也可被多个Activity 复用.Fragment可以响应自己的输入事件,并拥有自己的生命周期,但它们的生命周期直接被其所属的 ...

  5. ML:多变量线性回归(Linear Regression with Multiple Variables)

    引入额外标记 xj(i) 第i个训练样本的第j个特征 x(i) 第i个训练样本对应的列向量(column vector) m 训练样本的数量 n 样本特征的数量 假设函数(hypothesis fun ...

  6. C#每天进步一点--事件

    事件:如果类型定义了事件成员,那么类型就可以通知其他对象发生了特定的事情.例如,Button类提供了一个名为Click的事件.应用程序中的一个或者多个对象可能想接收关于这个事件的通知,以便在Butto ...

  7. delphi dispose释放内存的方法

    delphi dispose释放内存的方法 2010-06-08 19:39:59|  分类: DELPHI |  标签: |举报 |字号大中小 订阅     dispose使用方法的简单介绍在本文末 ...

  8. 条款16:成对使用new和delete时要使用相同的形式

    请牢记: 如果在new表达式中使用[],必须在相应的delete表达式中也使用[]. new[]  对应  delete[] 如歌在new表达式中不适用[],一定不要在相应的delete表达式中使用[ ...

  9. Spring Boot从入门到实战:集成AOPLog来记录接口访问日志

    日志是一个Web项目中必不可少的部分,借助它我们可以做许多事情,比如问题排查.访问统计.监控告警等.一般通过引入slf4j的一些实现框架来做日志功能,如log4j,logback,log4j2,其性能 ...

  10. windows环境利用hexo+github搭建个人博客

    一.下载安装Git 下载地址:https://gitforwindows.org/ 二.下载安装node.js 下载地址:https://nodejs.org/en/ 三.安装hexo 利用 npm ...