redis 命令 释放连接_Redis应用

最后这篇我们来一起看看Redis相关的两个应用:
  1. Redis消息队列

  2. 分布式锁

首先我们来说Redis消息,模式分为两种:
  1. 队列模式

  2. 发布订阅模式

队列模式使用list类型的lpush和rpop实现消息队列,具体的示意图如下:

e15b12599a3efc0fc9522b4a4f6b9183.png

需要注意的是消息接收方如果不知道队列中是否有消息,会一直发送rpop命令,如果这样的话,会每一次都建立一次连接,这样显然不好。可以使用brpop命令,它如果从队列中取不出来数据,会一直阻塞,在一定范围内没有取出则返回 null。另外一种是发布订阅模式,具体的示意图如下:

139c48ca7dacc9e7d102be99fc322ea0.png

接下来我们分析一下Redis的分布式锁。首先我们来看一个业务场景:在基于MQ的异步架构下,我们需要做防止用户重复下单。具体的示意图如下:

40ccefe63708bcd710afe3fa653d26b9.png

具体的业务逻辑步骤如下:
  1. 防止用户重复下单

  2. MQ消息去重

  3. 订单操作变更

  4. 库存超卖

哪些情况下我们需要使用锁,从上面业务场景我们可以抽象出如下业务模型:

  1. 共享资源
  2. 一次请求(处理)拥有唯一的Id

那么解决方案是什么?我们需要解决共享资源的互斥,共享资源的串行化问题,那么这其实就是锁的问题。

关于锁的处理又可以分为以下几种类型:

  1. 单应用中使用锁
  2. 分布式应用中使用锁

在单应用中使用锁我们可以使用最基础的synchronized或者高级一点的ReentrantLock。而在分布式应用中我们只能使用分布式锁。

Redis的分布式锁

首先我们来说一下Redis分布式锁实现的原理:利用Redis的单线程特性对特性的共享资源进行串行化处理。

获取锁

获取锁的实现方式有以下两种:
  1. 使用set命令实现

  2. 使用setnx命令实现

先来看set命令实现,这也是推荐的方式:
/*** 使用redis的set 命令实现获取分布式锁* @param lockKey 可以就是锁* @param requestId 请求ID,保证同一性 uuid+threadID* @param expireTime 过期时间,避免死锁* @return*/public boolean getLock(String lockKey,String requestId,int expireTime) {     //NX:保证互斥性    // hset 原子性操作    String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);     if("OK".equals(result)) {        return true;    }    return false;}
另外使用setnx命令实现:
public  boolean getLock(String lockKey,String requestId,int expireTime) {     Long result = jedis.setnx(lockKey, requestId);     if(result == 1) {         //成功设置 失效时间          jedis.expire(lockKey, expireTime);          return true;     }     return false;}
需要注意的是setnx会产生并发问题。

释放锁

常见的释放锁的方式有两种:
  1. del命令实现

  2. redis+lua脚本实现

使用del命令实现释放锁,具体实现如下:
/*** 释放分布式锁* @param lockKey * @param requestId */public static void releaseLock(String lockKey,String requestId) {    if (requestId.equals(jedis.get(lockKey))) {        jedis.del(lockKey);    }}

另外一种实现方式:
public static boolean releaseLock(String lockKey, String requestId) {        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end";        Object result = jedis.eval(script, Collections.singletonList(lockKey),Collections.singletonList(requestId));        if (result.equals(1L)) {            return true;        }        return false;}
推荐redis+lua脚本的方式,因为我们可以用lua脚本去保证命令执行的一致性。

生产环境中的分布式锁

落地生产环境用分布式锁,一般采用开源框架,比如Redisson。下面来讲一下Redisson对Redis分布式锁的实现。具体的实现方式如下: 首先我们加入jar包的依赖:
<dependency>    <groupId>org.redissongroupId>    <artifactId>redissonartifactId>    <version>2.7.0version>dependency>
接着我们对Redission进行配置:
public class RedissonManager {    private static Config config = new Config();     //声明redission对象    private static Redisson redisson = null;    //实例化redisson     static{        config.useClusterServers()         // 集群状态扫描间隔时间,单位是毫秒        .setScanInterval(2000)         //cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)         .addNodeAddress("redis://127.0.0.1:6379" )         .addNodeAddress("redis://127.0.0.1:6380")         .addNodeAddress("redis://127.0.0.1:6381")         .addNodeAddress("redis://127.0.0.1:6382")         .addNodeAddress("redis://127.0.0.1:6383")         .addNodeAddress("redis://127.0.0.1:6384");        //得到redisson对象        redisson = (Redisson) Redisson.create(config);}        //获取redisson对象的方法        public static Redisson getRedisson(){            return redisson;        }}
接着是锁的获取和释放:
public class DistributedRedisLock {     //从配置类中获取redisson对象    private static Redisson redisson = RedissonManager.getRedisson();    private static final String LOCK_TITLE = "redisLock_";     //加锁    public static boolean acquire(String lockName){         //声明key对象        String key = LOCK_TITLE + lockName;         //获取锁对象        RLock mylock = redisson.getLock(key);         //加锁,并且设置锁过期时间3秒,防止死锁的产生 uuid+threadId        mylock.lock(2,3,TimeUtil.SECOND); //加锁成功        return  true;    }            //锁的释放    public static void release(String lockName){        //必须是和加锁时的同一个key        String key = LOCK_TITLE + lockName;        //获取所对象        RLock mylock = redisson.getLock(key);        //释放锁(解锁)         mylock.unlock();    }}
然后在具体的业务逻辑中使用分布式锁:
public String discount() throws IOException{    String key = "test123";    //加锁 DistributedRedisLock.acquire(key);     //执行具体业务逻辑    dosoming    //释放锁     DistributedRedisLock.release(key);    //返回结果     return soming;}
最后我们来分析一下Redission实现分布式锁的原理,具体的示意图如下:

7116db69a61c20791c399200576ddd59.png

加锁机制

如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。发送lua脚本到redis服务器上,脚本如下:
"if (redis.call('exists',KEYS[1])==0) then "+"redis.call('hset',KEYS[1],ARGV[2],1) ; "+"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+"return nil; end ;" +"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+"return nil; end ;" +"return redis.call('pttl',KEYS[1]) ;"
Lua脚本的执行逻辑如下:第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。如何加锁呢?很简单,用下面的命令:hset myLock。通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:myLock:{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":1 }代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间,比如还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。然后关于锁的可冲入性这一块,第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”,此时就会执行可重入加锁的逻辑:
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
通过这个命令,对客户端1的加锁次数,累加1。数据结构会变成:
myLock:{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":2 }
最后关于锁的释放机制,执行lua脚本如下:
#如果key已经不存在,说明已经被解锁,直接发布(publish)redis消息 "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;" +# key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +# 将value减1"return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +# 如果counter>0说明锁在重入,不能删除key"if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; " +# 删除key并且publish 解锁消息"else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值