redis和redis-Java客户端的设计思路

为什么要写这篇博客?

记得第一次接触redis的时候,先是在官网redis在线调试 测试了一些简单的set、get 命令,立即就被吸引了,虽然以前没接触缓存技术,也没正式在项目中应用过,但是学习学习也不为过啊。

redis的安置和配置在网上有很多教程,真的得感谢万能的网友啊!其实官网也有教程,部署在linux上面的,简单清晰明了:
这里写图片描述

redis是用c编写的,代码简单整洁。如果我们想用好redis,或者给自己喜欢的语言编写一个redis的客户端,首先要搞明白的就是redids的通信协议和redis的数据类型,接下来我将一个一个介绍。

redis常用命令

1)连接操作命令
quit:关闭连接(connection)
auth:简单密码认证
help cmd: 查看cmd帮助,例如:help quit
2)持久化
save:将数据同步保存到磁盘
bgsave:将数据异步保存到磁盘
lastsave:返回上次成功将数据保存到磁盘的Unix时戳
shundown:将数据同步保存到磁盘,然后关闭服务
3)远程服务控制
info:提供服务器的信息和统计
monitor:实时转储收到的请求
slaveof:改变复制策略设置
config:在运行时配置Redis服务器
4)对value操作的命令
exists(key):确认一个key是否存在
del(key):删除一个key
type(key):返回值的类型
keys(pattern):返回满足给定pattern的所有key
randomkey:随机返回key空间的一个
keyrename(oldname, newname):重命名key
dbsize:返回当前数据库中key的数目
expire:设定一个key的活动时间(s)
ttl:获得一个key的活动时间
select(index):按索引查询
move(key, dbindex):移动当前数据库中的key到dbindex数据库
flushdb:删除当前选择数据库中的所有key
flushall:删除所有数据库中的所有key
5)String
set(key, value):给数据库中名称为key的string赋予值value
get(key):返回数据库中名称为key的string的value
getset(key, value):给名称为key的string赋予上一次的value
mget(key1, key2,…, key N):返回库中多个string的value
setnx(key, value):添加string,名称为key,值为value
setex(key, time, value):向库中添加string,设定过期时间time
mset(key N, value N):批量设置多个string的值
msetnx(key N, value N):如果所有名称为key i的string都不存在
incr(key):名称为key的string增1操作
incrby(key, integer):名称为key的string增加integer
decr(key):名称为key的string减1操作
decrby(key, integer):名称为key的string减少integer
append(key, value):名称为key的string的值附加value
substr(key, start, end):返回名称为key的string的value的子串
6)List
rpush(key, value):在名称为key的list尾添加一个值为value的元素
lpush(key, value):在名称为key的list头添加一个值为value的 元素
llen(key):返回名称为key的list的长度
lrange(key, start, end):返回名称为key的list中start至end之间的元素
ltrim(key, start, end):截取名称为key的list
lindex(key, index):返回名称为key的list中index位置的元素
lset(key, index, value):给名称为key的list中index位置的元素赋值
lrem(key, count, value):删除count个key的list中值为value的元素
lpop(key):返回并删除名称为key的list中的首元素
rpop(key):返回并删除名称为key的list中的尾元素
blpop(key1, key2,… key N, timeout):lpop命令的block版本。
brpop(key1, key2,… key N, timeout):rpop的block版本。
rpoplpush(srckey, dstkey):返回并删除名称为srckey的list的尾元素,并将该元素添加到名称为dstkey的list的头部
7)Set
sadd(key, member):向名称为key的set中添加元素member
srem(key, member) :删除名称为key的set中的元素member
spop(key) :随机返回并删除名称为key的set中一个元素
smove(srckey, dstkey, member) :移到集合元素
scard(key) :返回名称为key的set的基数
sismember(key, member) :member是否是名称为key的set的元素
sinter(key1, key2,…key N) :求交集
sinterstore(dstkey, (keys)) :求交集并将交集保存到dstkey的集合
sunion(key1, (keys)) :求并集
sunionstore(dstkey, (keys)) :求并集并将并集保存到dstkey的集合
sdiff(key1, (keys)) :求差集
sdiffstore(dstkey, (keys)) :求差集并将差集保存到dstkey的集合
smembers(key) :返回名称为key的set的所有元素
srandmember(key) :随机返回名称为key的set的一个元素
8)Hash
hset(key, field, value):向名称为key的hash中添加元素field
hget(key, field):返回名称为key的hash中field对应的value
hmget(key, (fields)):返回名称为key的hash中field i对应的value
hmset(key, (fields)):向名称为key的hash中添加元素field
hincrby(key, field, integer):将名称为key的hash中field的value增加integer
hexists(key, field):名称为key的hash中是否存在键为field的域
hdel(key, field):删除名称为key的hash中键为field的域
hlen(key):返回名称为key的hash中元素个数
hkeys(key):返回名称为key的hash中所有键
hvals(key):返回名称为key的hash中所有键对应的value
hgetall(key):返回名称为key的hash中所有的键(field)及其对应的value
9)sortset
// zadd key score member [[score member] [score member] …]
// 查询 zrange key start stop [withscores] //从小到大
// 统计在 zcount key min max
// 返回有续集key中member的score zscore key member
// 返回有续集key中指定范围[通过索引start stop]的member[及score],返回member根据score按降序排列
// zrevrange key start stop [withscores] //从大到小
// 根据score从低到高,返回member在有续集中的index zrank key member
// 根据score从高到低排序,返回member在有序集key中的index zrevrank key member

redis通信协议

这是官网的解释:

http://redis.io/topics/protocol

这是中文翻译之后的地址:

http://redis.readthedocs.org/en/latest/topic/protocol.html

redis客户端和服务器端主要通过tcp/ip连接来进行交互。如果把redis用java来改写的话就类似于有一个服务器端,端口号是6379,这样提供服务。

new ServerSocket(6379)

客户端调用的时候相当于:

new Socket(“127.0.0.1”, 6379);

其他语言也类似,只要遵守tcp/ip协议就可以。

服务器接收数据协议格式

cr和lf为换行符号\r\n

*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF

也就是说我们发给redis服务器的数据必须要遵守这个格式。比如

模拟命令 set gao gaoguangjin

        *3\r\n     (总共有3个参数 \r\n)
        $3\r\n     (第一个参数的字节数 \r\n)
        set\r\n     (第一个参数的值 \r\n)
        $3\r\n      (第二个参数的字节数 \r\n)
        gao\r\n     (第二个参数的值 \r\n)
        $11\r\n     (第三个参数的字节数 \r\n)
        gaoguangjin\r\n  (第三个参数的值 \r\n)

组装成字符串就是:
*3\r\n 3\r\nset\r\n 3\r\ngao\r\n$11\r\ngaoguangjin\r\n

也就是说我们按照tcp协议,给redis服务器端发送这条字符串,就相当于执行命令 set gao gaoguangjin。

服务器发送数据协议格式

客户端给服务器发送数据之后,服务器端肯定要给我们返回值的,当然返回值也是按照一定格式的

通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:
状态回复(status reply)的第一个字节是 “+
错误回复(error reply)的第一个字节是 “-
整数回复(integer reply)的第一个字节是 “:
批量回复(bulk reply)的第一个字节是 “$
多条批量回复(multi bulk reply)的第一个字节是 “*

我们来单个解释下
状态回复:

+OK

错误回复:

-ERR unknown command ‘foobar’
-WRONGTYPE Operation against a key holding the wrong kind of value

整数回复:返回整数回复的其中两个命令是 INCR 和 LASTSAVE
整数回复也被广泛地用于表示逻辑真和逻辑假,
比如 EXISTS 和 SISMEMBER 都用返回值 1 表示真, 0 表示假
SADD 、 SREM 和 SETNX , 只在操作真正被执行了的时候, 才返回 1 , 否则返回 0

批量回复###:

服务器使用批量回复来返回二进制安全的字符串,字符串的最大长度为 512 MB 。
比如:

客户端:GET mykey
服务器:foobar
服务器返回命令如:”$6\r\nfoobar\r\n”

如果被请求的值不存在, 那么批量回复会将特殊值 -1 用作回复的长度值, 就像这样:

客户端:GET non-existing-key
服务器:$-1

多条批量回复###:

客户端: LRANGE mylist 0 3
服务器: *4   (总共四条数据)
服务器: $3     
服务器: foo
服务器: $3
服务器: bar
服务器: $5
服务器: Hello
服务器: $5
服务器: World

我们来个Java代码测试

    private static void test() {
        try {
            Socket socket = new Socket("123.56.118.135", 6379);
            socket.setSoTimeout(3000);
            OutputStream outputStream = socket.getOutputStream();
            InputStream input = socket.getInputStream();
            byte[] by = new byte[1024];

            // 登陆密码
            outputStream.write("*2\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$4\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("AUTH\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$11\rn".getBytes(Protocol.CHARSET));
            outputStream.write("gaoguangjin\r\n".getBytes(Protocol.CHARSET));
            outputStream.flush();
            input.read(by);
            System.out.println("输出验证结果:" + new String(by));

            // 赋值 set gao gaoguangjin
            outputStream.write("*3\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$3\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("set\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$3\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("gao\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$11\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("gaoguangjin\r\n".getBytes(Protocol.CHARSET));

            outputStream.flush();
            input.read(by);
            System.out.println("输出set结果:" + new String(by));

            // 获取值get gao
            outputStream.write("*2\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$3\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("get\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$3\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("gao\r\n".getBytes(Protocol.CHARSET));
            outputStream.flush();
            input.read(by);
            System.out.println("输出get结果:" + new String(by));

            // lrange list 0 -1
            by = new byte[1024];
            outputStream.write("*4\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$6\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("lrange\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$4\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("list\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$1\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("0\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("$2\r\n".getBytes(Protocol.CHARSET));
            outputStream.write("-1\r\n".getBytes(Protocol.CHARSET));

            outputStream.flush();
            input.read(by);
            System.out.println("输出lrange结果:" + new String(by));
        } catch (Exception e) {
            log.error("连接失败!" + e.getLocalizedMessage());
        }
    }

结果:

输出验证结果:+OK
输出set结果:+OK
输出get结果:$11
gaoguangjin

输出lrange结果:*3
$2
n3
$2
n2
$2
n1

自己设计一个java客户端调用redis服务

redis java客户端已经有很多了,比如Jedis。使用redis的人那么多,为什么就jedis被官网推荐作为Java的客户端勒,当然有他的道理。我知道自己能力远不能达到独立设计一个类似于jedis的客户端,但是我们可以换种思路,如果让你去设计一个jedis你需要怎么做或者说你需要准备那几个步骤。
我的理解==》
准备步骤①:
这里写图片描述

准备步骤②
这里写图片描述

比如,如果我要执行最简单的命令 :

set name gaoguangjin
get name

第一步:我们Java客户端代码里面 需要有一段逻辑是 连接到服务器的

Socket socket = new Socket("123.56.118.135", 6379);

然后我们得到输入流和输出流。
利用输出流 我们把 set name gaoguangjin和 get name命令转换成redis服务器可以识别的字节码。所以我们这里需要一个方法专门用来封装转换命令到字节码的。

然后利用输入流得到 get name的返回值。返回值是redis给我们的一个字节数组。所以我们这里也需要一个专门用来封装redis字节数组到java需要的数据类型方法

如果redis服务器有设置密码的话,还需要发送密码验证。

一个redisjava客户端 就相当于一个 server客户端, 如果当客户端太多,并发量过多,每次都new Server()会导致占用大量不必要的系统开销。
所以我们需要设计一个类似于数据库连接池一样的 redisjava客户端连接池。

jedis源码探索之路

有时候思路有了,需要的就是参考哈哈,看看大牛们是怎么设计的。看源码我经历过三种状态,一种是为了纯粹打发时间的,或者说是为了 面试的时候装装B;一种是为了项目定位BUG的;最后一种是学习别人思想的。
他为什么要这样设计,是否用到了设计模式,如果不这样设计会有什么坏处,是否有冗余代码,他的设计有什么问题,如果自己要设计这样的项目,哪些地方会是难点,其实就是带着问题去看源码,然后去源码里面找到你想要的知识点。
jedis源码目录
jedis源码目录

uml图片
这里写图片描述

感觉把uml图片一花基本就清楚多了。虽然我是第一次画哈哈。
这里写图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值