为什么要写这篇博客?
记得第一次接触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通信协议
这是官网的解释:
这是中文翻译之后的地址:
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的;最后一种是学习别人思想的。
他为什么要这样设计,是否用到了设计模式,如果不这样设计会有什么坏处,是否有冗余代码,他的设计有什么问题,如果自己要设计这样的项目,哪些地方会是难点,其实就是带着问题去看源码,然后去源码里面找到你想要的知识点。
uml图片
感觉把uml图片一花基本就清楚多了。虽然我是第一次画哈哈。