Redis的eval命令之Lua脚本

场景延伸:
1.判断A状态是否为0,为0才放行
2.变更A状态为1
3.执行任务
4.填充任务结果
5.复原A状态为0

现在有四个任务,同时有两个线程,会存在两个线程T1和T2都能通过了第一步。此时T2先行,执行通过第5步,将A的状态变更为0。T1继续运行,又将状态变更0,走后续步骤。
在这里插入图片描述
问题: 其中的1只能放行一个任务,结果出现了多任务都可以通过,并且任务结果会被后续的T1给覆盖掉了。那么怎么能限制只能放行一个任务呢?

对1、2进行加锁(可以自定义各种🔒,例如synchronizeReentranlock因为此文说明Redis,所以用了Redis锁)。

String userId = String.valueOf(user.getUserId());
        // 🔒Start 判断用户是否有提交任务 (需要上锁,防止并发线程隐患)
        redisGateway.lockSettleListStart(userId);
        if (Boolean.TRUE.equals(judgeStatus(StartStatusEnum.START_STATUS_STARTED,userId))) {
            redisGateway.unlockSettleListStart(userId);
            throw new BizException("请取消当前提交任务后再提交");
        }
        redisGateway.saveStartStatusByUserId(userId, StartStatusEnum.START_STATUS_STARTED.getType());
        redisGateway.unlockSettleListStart(userId);
        // 🔒End

Redis底层的上锁和解锁:

/**
     * @description
     * @param key: 锁键
     * @param id: 随机id
     * @param expireTime: 锁过期时间
     * @param timeout: 获取锁超时时间
     * @return: boolean
     */
    public boolean lock(String key, String id, long expireTime, long timeout) {
        SetParams params = SetParams.setParams().nx().px(expireTime);
        Jedis jedis = jedisPool.getResource();
        Long start = System.currentTimeMillis();
        try {
            for (; ; ) {
                //SET命令返回OK ,则证明获取锁成功
                String lock = jedis.set(key, id, params);
                if ("OK".equals(lock)) {
                    return true;
                }
                //否则循环等待,在timeout时间内仍未获取到锁,则获取失败
                long l = System.currentTimeMillis() - start;
                if (l >= timeout) {
                    return false;
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } finally {
            jedis.close();
        }
    }

    /**
     * 解锁
     *
     * @param id
     * @return
     */
    public boolean unlock(String key, String id) {
        Jedis jedis = jedisPool.getResource();
        String script =
                "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                        "   return redis.call('del',KEYS[1]) " +
                        "else" +
                        "   return 0 " +
                        "end";
        try {
            Object result = jedis.eval(script, Collections.singletonList(key),
                    Collections.singletonList(id));
            return "1".equals(result.toString());
        } finally {
            jedis.close();
        }
    }
  

这样1、2步就限制单个线程进入,例如T1进入后修改状态成1,T2再想进去lock的时候redis已经存在该键,一定的时间内无法上锁,则抛异常,不给通过。

除了上锁还能怎么操作呢?实际上我们只要保证查状态和更改状态是一个原子性操作就行了。也就是判断状态和更改状态要么一起运行要么都不运行。这里我们采用今天的主角Rediseval命令之Lua脚本
在这里插入图片描述

结论先行:
// 阻塞轮询取消
        Boolean changeStatusResult = redisGateway.casStartStatusByUser(userId,
        StartStatusEnum.START_STATUS_NOT_STARTED.getType(),
        StartStatusEnum.START_STATUS_STARTED.getType(),
        true);
        // 更改状态失败
         if (Boolean.FALSE.equals(changeStatusResult)) {
            throw new BizException("当前任务暂未完成,请稍后重试");
        }

Redis底层CAS原子🔒

/**
     * CAS锁,理解成AtomicReference
     * @param key
     * @param expect
     * @param update
     * @param expireTime
     * @param timeout
     * @param condition 同或异
     * @return
     */
    public Boolean CASLock(String key, String expect, String update, int expireTime, long timeout,Boolean condition) {
        Jedis jedis = jedisPool.getResource();
        String eval = null;
        if (condition) {
            eval = "==";
        }else {
            eval = "!=";
        }
        Long start = System.currentTimeMillis();
        try {
            for (; ; ) {
                String script =
                        "if redis.call('get',KEYS[1]) "+eval+" ARGV[1] " +
                                "or not(redis.call('get', KEYS[1])) then"+
                                "   return redis.call('set',KEYS[2],ARGV[2]) " +
                                "else" +
                                "   return 0 " +
                                "end";
                Object result = jedis.eval(script, 2,key,key,expect,update);
                Boolean end = "OK".equals(result.toString());
                if (Boolean.TRUE.equals(end)) {
                    jedis.expire(key, expireTime);
                    return true;
                }
                //否则循环等待,在timeout时间内仍未获取到锁,则获取失败
                long l = System.currentTimeMillis() - start;
                if (l >= timeout) {
                    return false;
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } finally {
            jedis.close();
        }
    }
解析
Redis-Eval 命令

语法:redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:redis.call(),redis.pcall()

127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) == ARGV[1] or not(redis.call('get',KEYS[1])) then return redis.call('set',KEYS[2],ARGV[2]) else return 0 end" 2 user user whh whh1
OK

127.0.0.1:6379> keys *
 1) "LOGIN_TOKEN_KEY:500"
 2) "LOGIN_TOKEN_KEY:504"
 3) "LOGIN_TOKEN_KEY:501"
 4) "LOGIN_TOKEN_KEY:508"
 5) "LOGIN_TOKEN_KEY:510"
 6) "LOGIN_TOKEN_KEY:505"
 7) "LOGIN_TOKEN_KEY:506"
 8) "LOGIN_TOKEN_KEY:509"
 9) "LOGIN_TOKEN_KEY:507"
10) "LOGIN_TOKEN_KEY:502"
11) "user"
12) "LOGIN_TOKEN_KEY:503"
127.0.0.1:6379> eval "return not(redis.call('get',KEYS[1]))" 1 KEY1 VA
(integer) 1
127.0.0.1:6379> eval "return not(redis.call('get',KEYS[1]))" 1 user VA
(nil)
127.0.0.1:6379>  eval "if redis.call('get',KEYS[1]) == ARGV[1] or not(redis.call('get',KEYS[1])) then return redis.call('set',KEYS[2],ARGV[2]) else return 0 end" 2 user user whh whh1
(integer) 0
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) == ARGV[1] or not(redis.call('get',KEYS[1])) then return redis.call('set',KEYS[2],ARGV[2]) else return 0 end" 2 user1 user qwe whh1
OK

其中KEYS[1]ARGV[1]对应的就是参数keyarg中的第一个。该句的业务是,如果redis中keys为user值为whh 或者为 null,则更新为whh1 否则返回0。

not(redis.call('get',KEYS[1]))用于判断是否为null,redis.call('get',KEYS[1]) 如果没有该key,则返回 nil(false),not为取反。所以为null则返回(1)ture

>> Redis中LUA判空案例

>> Redis中LUA限流案例

jedis.eval源码

在这里插入图片描述

源码Code


1
package redis.clients.jedis;
类:Jedis
@Override
  public Object eval(final String script, final int keyCount, final String... params) {
    //检查管道和多通道是否开启,jedis不支持,会抛异常。
    checkIsInMultiOrPipeline(); 
    client.eval(script, keyCount, params);
    client.setTimeoutInfinite();
    try {
      return getEvalResult();
    } finally {
      client.rollbackTimeout();
    }
  }
  
2
package redis.clients.jedis;
类:BinaryJedis
protected void checkIsInMultiOrPipeline() {
    if (client.isInMulti()) {
      throw new JedisDataException(
          "Cannot use Jedis when in Multi. Please use Transaction or reset jedis state.");
    } else if (pipeline != null && pipeline.hasPipelinedResponse()) {
      throw new JedisDataException(
          "Cannot use Jedis when in Pipeline. Please use Pipeline or reset jedis state .");
    }
  }

3
package redis.clients.jedis;
类:Client
 public void eval(final String script, final int keyCount, final String... params) {
    eval(SafeEncoder.encode(script), toByteArray(keyCount), SafeEncoder.encodeMany(params));
  }

4
package redis.clients.jedis.util;
类:SafeEncoder 调用这个方法用特定的UTF-8编码转成想要的字符
 public static byte[] encode(final String str) {
    try {
      if (str == null) {
        throw new JedisDataException("value sent to redis cannot be null");
      }
      return str.getBytes(Protocol.CHARSET);
    } catch (UnsupportedEncodingException e) {
      throw new JedisException(e);
    }
  }

5
package redis.clients.jedis;
类:BinaryClient
  public void eval(final byte[] script, final byte[] keyCount, final byte[][] params) {
    sendCommand(EVAL, joinParameters(script, keyCount, params));
  }
  >>>>>>>joinParameters 就是eval命令中的三个参数转换成对应的二维数组。<<<<<
  private byte[][] joinParameters(byte[] first, byte[] second, byte[][] rest) {
    byte[][] result = new byte[rest.length + 2][];
    result[0] = first;
    result[1] = second;
    System.arraycopy(rest, 0, result, 2, rest.length);
    return result;
  } 

6 
通过connection类
package redis.clients.jedis;
类:Connection
 public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
    try {
    >>>>>此处jedis开启socket连接(TCP)<<<<<
      connect(); 
      >>>>>此处jedis通过输出流发送信息(TCP)<<<<<
      Protocol.sendCommand(outputStream, cmd, args);
    } catch (JedisConnectionException ex) {
      /*
       * When client send request which formed by invalid protocol, Redis send back error message
       * before close connection. We try to read it to provide reason of failure.
       */
      try {
        String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
        if (errorMessage != null && errorMessage.length() > 0) {
          ex = new JedisConnectionException(errorMessage, ex.getCause());
        }
      } catch (Exception e) {
        /*
         * Catch any IOException or JedisConnectionException occurred from InputStream#read and just
         * ignore. This approach is safe because reading error message is optional and connection
         * will eventually be closed.
         */
      }
      // Any other exceptions related to connection?
      broken = true;
      throw ex;
    }
  }

 public void connect() {
    if (!isConnected()) {
      try {
        socket = jedisSocketFactory.createSocket();

        outputStream = new RedisOutputStream(socket.getOutputStream());
        inputStream = new RedisInputStream(socket.getInputStream());
      } catch (IOException ex) {
        broken = true;
        throw new JedisConnectionException("Failed connecting to "
            + jedisSocketFactory.getDescription(), ex);
      }
    }
  }

7
 private static void sendCommand(final RedisOutputStream os, final byte[] command,
      final byte[]... args) {
    try {
      os.write(ASTERISK_BYTE);
      os.writeIntCrLf(args.length + 1);
      os.write(DOLLAR_BYTE);
      os.writeIntCrLf(command.length);
      os.write(command);
      os.writeCrLf();

      for (final byte[] arg : args) {
        os.write(DOLLAR_BYTE);
        os.writeIntCrLf(arg.length);
        os.write(arg);
        os.writeCrLf();
      }
    } catch (IOException e) {
      throw new JedisConnectionException(e);
    }
  }

8
str.getBytes(Protocol.CHARSET) //通过Protocal中指定的编码格式去转换
Protocol类中的数据是一些redis操作的命令和参数的封装类


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值