前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一篇和Redis有关的锁和事务的文章

一篇和Redis有关的锁和事务的文章

作者头像
_淡定_
修改2019-05-16 10:58:53
1K0
修改2019-05-16 10:58:53
举报
文章被收录于专栏:dotnet & javadotnet & java

部分参考链接 Transaction StackExchange.Redis Transaction hashest

正文

Redis 是一种基于内存的单线程数据库。意味着所有的命令是一个接一个的执行。


考虑只有一个Redis实例,也就是Redis本身没有做分布式。


通过SETNX命令,set if not exist的缩写。那么多个服务在调用的时候可以通过同一个key申请一个lock(也就是调用命令成功返回1),然后根据相应条件做释放(比如时间到期,or手动释放),也就是delete key。

Redis本身有MULTI命令,标记开启一个事务。开启之后后面的命令会在调用EXEC命令的时候以一个集合的方式整体执行,也就是原子性(不保证整体的成功or失败,只是都会去尝试执行)。

现在有个需求,用redis实现Check and Set,也就是先读取里面的值,然后设置(比如做个+=val);并发的问题是必须要考虑的。

用redis描述大致是这样的。这里假设redis没有incr这个自增命令。

val = GET mykey
val = val + 1
SET mykey $val

直接这样做,并发问题是肯定有的。所以,按照上面的知识,应该有2种方法来避免这个并发问题。

基于SENTX命令。

copy一下文档的demo

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> 

第一次调用setnx,设置mykey的value为hello,返回1,表示成功。

第二次调用setnx,设置mykey的value为world,因为第一次调用并没有释放mykey,所以返回0,表示设置失败。

最后获取mykey的值,返回的是hello。

最后记得要去释放mykey。

这其实是一个悲观锁,也就是一个进程获取到锁之后要等释放别的进程才能继续。

基于MULTI命令。

先看一个简单的应用

 127.0.0.1:6379> multi 
 OK
 127.0.0.1:6379> incr foo 
 QUEUED
 127.0.0.1:6379> incr bar 
 QUEUED
 127.0.0.1:6379> exec 
 1) (integer) 1 
 2) (integer) 1
 

第一步调用MULTI命令,表示开始多个命令的输入。返回OK,表示开始接收。

第二步调用incr foo,给foo对应的值做自增。返回queued,表示已加入队列。

第三步调用incr bar,给bar对应的值做资政,返回queued,表示已加入队列。

最后调用exec命令,表示执行队列中的命令。返回每个命令的结果。

1.有错误了怎么办

首先错误分两种

- 在enqueue的时候出错,最常见的就是参数错误。比如下面这个例子
 127.0.0.1:6379> multi
  OK 
  127.0.0.1:6379> set a 1234 
  QUEUED 
  127.0.0.1:6379> set a 1 1 1 1 1 1 11 
  QUEUED 
  127.0.0.1:6379> exec 
  1) OK
  2) (error) ERR syntax error 
  127.0.0.1:6379>
 
 

第二个set a 11111111命令是有语法错误,所以,在执行exec的时候会返回语法错误。第一个是成功的。所以,如果在后面get a是会返回1234,为成功的设置。 假设报错的命令在中间,后面的命令也是会执行的。

- 还有就是直接命令就不对的。看个例子
 127.0.0.1:6379> multi 
 OK
 127.0.0.1:6379> set a 11 
 QUEUED
 127.0.0.1:6379> aaa 
 (error) ERR unknown command `aaa`, with args beginning with:
 127.0.0.1:6379> exec 
 (error) EXECABORT Transaction discarded because of previous errors.
 

先set a,进入队列。

执行aaa命令,这个命令不存在。

直接报错。 执行exec,事务因为之前的错误,exec中止。

3.为什么没有回滚

通过上面的例子,看到redis对multi的操作是没有回滚的,或许有点奇怪。根据文档描述,有两个原因。

- redis的命令执行只有在语法错误或者数据类型出错的时候会失败,而不是在enqueue的时候。这意味着失败是由程序设置错误导致的。那么,这种错误肯定是在开发环境中就应该容易被发现,而不是在生产环境。
- 为了快。

4.WATCH 命令的乐观锁 结合watch命令我们也可以实现上面的需求。

 WATCH mykey
 --Begin---
 ##下面两行是客户端命令 
 val = GET mykey
 val = val + 1 
 --End---
 MULTI SET mykey $val 
 EXEC

解释一下,先获取一下mykey的监控。然后客户端获取mykey的值,(是客户端,不是命令服务端)。然后赋值自增。然后服务端开启MULTI, 设置新的值。执行。

假设在MULTI和Exec之间,mykey的值被别的client修改,exec会返回(nil)。

下面做个演示: 先在redis-cli上执行以下命令

127.0.0.1:6379> watch a 
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379>set a 13 
QUEUED  

如上,已经开启WATCH,然后设置a =13 进入队列。

然后在本地的redis desktop manager上去修改这个值。

然后再在服务器上执行 exec,

127.0.0.1:6379> exec
(nil)

返回的是nil,表示没有成功。如果没有客户端去更新,执行exec是返回OK。

5.redis-scripting-and-transactions

在Redis 2.6之后,引入了Redis script来实现事务的功能。通常来说script方式速度会相对快一点(没有做测试)。不过既然multi已经出来很久了,所以,不太可能会移除这个命令。

在StackExchange.Redis中使用

显然,也分两种,基于setnx 或者 MULTI + WATCH。分别对应的是IDatabaseAsync.LockTakeAsyncIDatabaseAsync.CreateTransaction这里结合了Polly这个库用于重试,毕竟,悲观锁,我多拿几次总能拿到的;乐观锁,执行的命令,我多试几次,总能成功的。

LockTakeAsync

public async Task<T> TakeLockAsync<T>(string key, string token, Func<object, Task<T>> func, object obj)
    where T : class
{
    var db = GetDb(redisConfigModel.LockDbIndex);//获取IDatabaseAsync对象
    //定义获取锁的策略
    var policy = Policy
        .HandleResult<bool>(w => !w)
        .WaitAndRetryForeverAsync(
            sleepDurationProvider: attemp => TimeSpan.FromSeconds(3), //两次重复尝试的间隔
            onRetry: (delegeteRst, ts) =>
            {
                //可以记录日志啥的
            }
        );
    //竞争获取锁。
    await policy.ExecuteAsync(async () => await db.LockTakeAsync(key, token, TimeSpan.MaxValue));  
    try
    {
        return await func(obj);//获取到锁之后的具体执行的方法。
    }
    finally
    {
        await db.LockReleaseAsync(key, token); //最后一定要释放
    }
}

LockTakeAsync的时候根据key对应的token值是否已经被获取来作为条件。

CreateTransaction

StackExchange.Redis 用multiplexer类实现Redis的一些列命令。我们的代码不能直接简单的映射到watch命令,因为,单纯调用watch是肯定成功的,这样会导致大家都"成功"(假的)。这里用的Condition的方式来实现。

public async Task AddAfterReadAsync(string key, int value, string hashField = "hash_field")
{
      //处理policy的结果为false的情况,一直重试。
    var policy = Policy.HandleResult<bool>(w => !w).RetryForeverAsync();
      //执行
    await policy.ExecuteAsync(async () =>
    {
        var db = GetDb(redisConfigModel.LockDbIndex);
        var trans = db.CreateTransaction();
        var oldValue = Convert.ToInt32(await db.StringGetAsync(key));
        trans.AddCondition(Condition.HashNotExists(key,
            hashField)); //这里确保hashField不存在。也可以用Condition.KeyNotExists(key)
        //这里不能await,因为每个命令的结果只有在执行了execute后才知道。
        trans.StringSetAsync(key, (oldValue + value).ToString());
        var execSuccess = await trans.ExecuteAsync();
        return execSuccess;
    });
}

这是一篇和redis有关的锁,事务的文章。写了我一整个下午。看完,感觉也没有多少东西。感觉开头链接中关于hashset还是有点意思的。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-05-10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 正文
    • 基于SENTX命令。
      • 基于MULTI命令。
        • 在StackExchange.Redis中使用
        相关产品与服务
        云数据库 Redis
        腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档