Redis使用教程之jedis客户端sendCommand方法的byte[]入参,防止混淆string的byte与数值byte的区别

文章讲述了在使用Jedis的sendCommand方法操作Redis时,遇到的特殊参数类型问题,如zset的score和Pexpireat命令的时间戳。sendCommand方法通常接收字符串命令的字节数组,但时间戳作为long类型在Redis中以二进制存储。转换过程中需要注意将long时间戳转化为字符串的字节数组,以正确地与Redis服务器通信。
摘要由CSDN通过智能技术生成

1.背景

在使用jedis客户端操作redis数据的过程中,发现了一个通用的方法sendCommand,封装了redis的所有命令,代码路径:redis.clients.jedis.BinaryJedis#sendCommand(redis.clients.jedis.commands.ProtocolCommand, byte[]...),具体代码如下

    public Object sendCommand(ProtocolCommand cmd, byte[]... args) {
        this.checkIsInMultiOrPipeline();
        this.client.sendCommand(cmd, args);
        return this.client.getOne();
    }

2.封装使用sendCommand方法

封装代码

public void sendCommandWrapper(List<List<byte[]>> commandList) {
            Jedis jedis = new Jedis("127.0.0.1",3306);
            for (List<byte[]> command : commandList) {
                byte[][] splitResult = command.stream().toArray(byte[][]::new);
                for (byte[] cmd : command) {
                   jedis.sendCommand(() -> splitResult[0],
                        Arrays.copyOfRange(splitResult, 1, splitResult.length));
            }
    }
}

方法入参

参数类似如下

[[["set"],["a"],["v"]],[["set"],["a"],["b"]]],[["set"],["a"],["1"]]]]

然后把每个字符换成byte

[[[115, 101, 116], [97], [118]], [[115, 101, 116], [97], [98]], [[115, 101, 116], [97], [49]]]

3.存在的问题

sendCommand方法可以传递string的命令转成的byte[]参数,但是其中有两个特例

需要注意的特例

zset的score是有符号的浮点型

Pexpireat key 时间戳:时间戳是long型

问题归纳

以“Pexpireat key 时间戳”命令为例,long型时间戳通过redis协议到redis底层存储是byte[]的方式,使用sendCommand的时候传递的命令入参也是byte[],但是这两种byte[]不是同一种byte[]。

这两种 byte[] 不同的主要原因是它们所表示的含义不同。

在使用 Redis 命令时,我们需要将命令的参数转换为 byte[] 格式,以便可以发送给 Redis 服务器。这里的 byte[] 实际上是字符串的字节数组表示。Redis 协议是基于文本的,即它要求在与服务器通信时发送文本字符串,因此发送给 Redis 服务器的 byte[] 实际上是表示字符串的字节数组。

String.valueOf(redisTTLLong).getBytes(StandardCharsets.UTF_8)

而在 Redis 底层存储中,时间戳所表示的含义是一个数字,而不是一个字符串。在底层存储中,Redis 将时间戳转换为了二进制形式,即一个 byte[] 数组。这个 byte[] 数组表示的是一个数字,它在内存中以二进制补码的形式存储。

//比如从底层取出来的byte,想转回时间戳需要的转换逻辑: byte转long 
private static long convertRedisTTLToTimestamp(byte[] ttlBytes) {
        // Convert the byte array to an 8-byte binary string
        byte[] binaryBytes = new byte[8];
        for (int i = 0; i < 8; i++) {
            binaryBytes[i] = i < ttlBytes.length ? ttlBytes[i] : 0;
            if (binaryBytes[i] < 0) {
                binaryBytes[i] += 256;
            }
        }
        // Rearrange the binary string according to the big endian byte order
        long timestamp = 0L;
        for (int i = 0; i < 8; i++) {
            timestamp = (timestamp << 8) + (binaryBytes[i] & 0xff);
        }
        // returns the converted timestamp in milliseconds
        return timestamp;
    }

因此,这两种 byte[] 不同的原因在于它们所表示的含义不同。一个表示字符串,一个表示数字的二进制补码。虽然它们都是 byte[] 类型,但它们的内部存储和解析方式是不同的

4.总结

使用jedis的sendCommand命令时,要记住要传入的参数,原本属于数值类型时,需要转byte[]数组是直接转成字符串的字节数组。也就是把long时间戳通过String.valueOf(redisTTLLong).getBytes(StandardCharsets.UTF_8)命令转换出来的byte[]。

请勿与long转byte等相关的补码、大小端等概念混淆。

Jedis使用总结 前段时间细节的了解了Jedis的使用,Jedisredis的java版本的客户端实现。 本文做个总结,主要分享如下内容: 【pipeline】【分布式的id生成器】【分布式锁【watch】【multi】】【redis分布式】 好了,一个一个来。 一、 Pipeline 官方的说明是:starts a pipeline,which is a very efficient way to send lots of command and read all the responses when you finish sending them。简单点说pipeline适用于批处理。当有大量的操作需要一次性执行的时候,可以用管道。 示例: Jedis jedis = new Jedis(String, int); Pipeline p = jedis.pipelined(); p.set(key,value);//每个操作都发送请求给redis-server p.get(key,value); p.sync();//这段代码获取所有的response 这里我进行了20w次连续操作(10w读,10w写),不用pipeline耗时:187242ms,用pipeline耗时:1188ms,可见使用管道后的性能上了一个台阶。看了代码了解到,管道通过一次性写入请求,然后一次性读取响应。也就是说jedis是:request response,request response,...;pipeline则是:request request... response response的方式。这样无需每次请求都等待server端的响应。 二、 跨jvm的id生成器 谈到这个话题,首先要知道redis-server端是单线程来处理client端的请求的。 这样来实现一个id生成器就非常简单了,只要简单的调用jdeis.incr(key);就搞定了。 你或许会问,incr是原子操作吗,能保证不会出现并发问题吗,不是说了吗,server端是单线程处理请求的。 三、 【跨jvm的锁实现【watch】【multi】】 首先说下这个问题的使用场景,有些时候我们业务逻辑是在不同的jvm进程甚至是不同的物理机上的jvm处理的。这样如何来实现不同jvm上的同步问题呢,其实我们可以基于redis来实现一个锁。 具体事务和监听请参考文章:redis学习笔记之事务 暂时找到三种实现方式: 1. 通过jedis.setnx(key,value)实现 import java.util.Random; import org.apache.commons.pool.impl.GenericObjectPool.Config; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Transaction; /** * @author Teaey */ public class RedisLock { //加锁标志 public static final String LOCKED = "TRUE"; public static final long ONE_MILLI_NANOS = 1000000L; //默认超时时间(毫秒) public static final long DEFAULT_TIME_OUT = 3000; public static JedisPool pool; public static final Random r = new Random(); //锁的超时时间(秒),过期删除 public static final int EXPIRE = 5 * 60; static { pool = new JedisPool(new Config(), "host", 6379); } private Jedis jedis; private String key; //锁状态标志 private boolean locked = false; public RedisLock(String key) { this.key = key; this.jedis = pool.getResource(); } public boolean lock(long timeout) { long nano = System.nanoTime(); timeout *= ONE_MILLI_NANOS; try { while ((System.nanoTime() - nano) < timeout) { if (jedis.setnx(key, LOCKED) == 1) { jedis.expire(key, EXPIRE); locked = true; return locked; } // 短暂休眠,nano避免出现活锁 Thread.sleep(3, r.nextInt(500)); } } catch (Exception e) { } return false; } public boolean lock() { return lock(DEFAULT_TIME_OUT); } // 无论是否加锁成功,必须调用 public void unlock() { try { if (locked) jedis.del(key); } finally { pool.returnResource(jedis); } } } 2. 通过事务(multi)实现 由于采纳第一张方法,第二种跟第三种实现只贴了关键代码,望谅解。^_^ public boolean lock_2(long timeout) { long nano = System.nanoTime(); timeout *= ONE_MILLI_NANOS; try { while ((System.nanoTime() - nano) < timeout) { Transaction t = jedis.multi(); // 开启事务,当server端收到multi指令 // 会将该client的命令放入一个队列,然后依次执行,知道收到exec指令 t.getSet(key, LOCKED); t.expire(key, EXPIRE); String ret = (String) t.exec().get(0); if (ret == null || ret.equals("UNLOCK")) { return true; } // 短暂休眠,nano避免出现活锁 Thread.sleep(3, r.nextInt(500)); } } catch (Exception e) { } return false; } 3. 通过事务+监听实现 public boolean lock_3(long timeout) { long nano = System.nanoTime(); timeout *= ONE_MILLI_NANOS; try { while ((System.nanoTime() - nano) < timeout) { jedis.watch(key); // 开启watch之后,如果key的值被修改,则事务失败,exec方法返回null String value = jedis.get(key); if (value == null || value.equals("UNLOCK")) { Transaction t = jedis.multi(); t.setex(key, EXPIRE, LOCKED); if (t.exec() != null) { return true; } } jedis.unwatch(); // 短暂休眠,nano避免出现活锁 Thread.sleep(3, r.nextInt(500)); } } catch (Exception e) { } return false; } 最终采用第一种实现,因为加锁只需发送一个请求,效率最高。 四、 【redis分布式】 最后一个话题,jedis的分布式。在jedis的源码里发现了两种hash算法(MD5,MURMUR Hash(默认)),也可以自己实现redis.clients.util.Hashing接口扩展。 List hosts = new ArrayList(); //server1 JedisShardInfo host1 = new JedisShardInfo("", 6380, 2000); //server2 JedisShardInfo host2 = new JedisShardInfo("", 6381, 2000); hosts.add(host1); hosts.add(host2); ShardedJedis jedis = new ShardedJedis(hosts); jedis.set("key", "");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值