我们之前的学习都是通过Redis自带的命令行式的客户端来使用Redis的,我们在执行命令的时候,都是手动执行的。然而这种操作方式并不是日常开发的主要形式。
更多的时候,是使用Redis的api来实现定制化的Redis客户端程序,进一步来操作服务器。
虽然还有些带有图形化界面的客户端,它们的本质跟命令行式的客户端一样,都是通用性质的,在工作中还是会更希望用到专用的,定制化的客户端程序。
认识RESP
说到自定义客户端,我们为什么能够自定义客户端呢?对于qq,微信这样的app,我们又能不能自定义客户端呢?肯定是不能的,因为这些程序没有公开自己使用的自定义协议。
我们能够对Redis服务自定义客户端,是因为Redis自定义的应用层协议是公开的。
名字就叫做RESP。
在官网文档这里描述了这个协议的优点:
1.简单好实现。
2.快速进行解析。
3.肉眼可以阅读的。
Redis服务器,传输层是基于TCP协议的,但是和TCP又没有强耦合的。
这里的RESP我们只需要简单了解就可以了,因为目前已经有很多大佬实现了对这套协议的解析/构造了,我们只需要拿着大佬们提供的库就可以较为方便的完成与Redis服务器的通信操作了。
安装 redis-plus-plus
在GitHub上
另外,在使用这个库之前,还需要再安装一个C语言的Redis客户端的库,因为redis-plus-plus依赖这个库。
就是这个hiredis,安装方式有两种:
1.源码安装(比较麻烦)
需要我们把源码下过来,然后进行编译安装,这种方式容易出幺蛾子,比如环境兼容问题之类的
安装这个我们可以直接使用包管理器来安装
Ubuntu下
apt install libhiredis-dev
CentOS下
yum install hiredis-devel.x86_64
注意需要管理员权限。
安装完hiredis后,就正式安装redis-plus-plus了。
redis-plus-plus只能用源码安装的方式,另外在Ubuntu下安装要比CentOS下安装要容易一些,因为CentOS下许多软件的版本太老了,环境可能不大兼容,所以这里重点说明在Ubuntu下的安装。
官网给出的源码安装方法:
git clone https://github.com/sewenew/redis-plus-plus.git
cd redis-plus-plus
mkdir build
cd build
cmake ..
make
make install
cd ..
另外,学到这里我们需要学会一个新的构建工具,以前我们都是使用的makefile,但是makefile太简陋,原始了,makefile需要程序员手写,这样效率太低了。
Cmake是一个自动构建makefile的工具,通过程序来生成makefile,现在实际开发很少使用makefile了。
使用Cmake,可以先创建一个目录,然后进入到这个目录中,这个步骤并不是必须的,而是因为Cmake在生成的时候会有很多中间文件,有可能会污染源文件,所以特地把生成的文件放到一个专门的目录下。
如果Ubuntu下没有安装Cmake,可以安装一下:
apt install cmake
make安装好后就是这个样子
另外关于这个make install的作用,在我们编译完成后
像.a .so这样库都是在当前编译目录下的,后续写代码时不一定找得到这里的库,因此推荐把这些库拷贝到系统目录中,手动拷贝比较麻烦,而Cmake早就帮我们做好了,直接执行
make install
就可以自动的帮我们把这些库拷贝到系统目录下了。
最后,在编写代码的时候,要使用这个库,并且刚刚的安装的日志找不到了,可以使用find命令来查找
第一个是头文件,第二个是库。
find查找redis++库的示例:
find /usr/ -name "redis++*"
一般安装的库头文件都会放在/usr/ 下。
简单命令的使用
ping
hello.cc
#include <iostream>
#include <sw/redis++/redis++.h>
using std::cout;
using std::endl;
int main()
{
sw::redis::Redis redis("tcp://127.0.0.1:6379");
std::string ret = redis.ping();
cout << ret << endl;
return 0;
}
在初始化redis对象时,用了一个url,也就是唯一资源定位符。url确实是Http协议用的最多,但并不是Http专属的。
makefile
如果在CentOS上,这些库的位置可能不一样,但是查找库的方式是一样的(用find)
hello:hello.cc
g++ -std=c++17 -o $@ $^ /usr/local/lib/libredis++.a /usr/lib/x86_64-linux-gnu/libhiredis.a -pthread
.PHONY:clean
clean:
rm -f hello
这里注意要把库的位置写上。
get/set
先看代码:
generic.cc
#include <iostream>
#include <sw/redis++/redis++.h>
using std::cout;
using std::endl;
void test(sw::redis::Redis& redis)
{
redis.flushall(); // 先清空数据库,避免干扰
redis.set("key1","11");
redis.set("key2","22");
redis.set("key3","33");
auto val1 = redis.get("key1");
auto val2 = redis.get("key2");
auto val3 = redis.get("key3");
auto val4 = redis.get("key4");
if(val1)
{
cout << "val1 = " << val1.value() << endl;
}
if(val4)
{
cout << "val4 = " << val4.value() << endl;
}
}
int main()
{
sw::redis::Redis redis("tcp://127.0.0.1:6379");
test(redis);
return 0;
}
makefile
.PHONY:all
all:hello generic
hello:hello.cc
g++ -std=c++17 -o $@ $^ /usr/local/lib/libredis++.a /usr/lib/x86_64-linux-gnu/libhiredis.a -pthread
generic:generic.cc
g++ -std=c++17 -o $@ $^ /usr/local/lib/libredis++.a /usr/lib/x86_64-linux-gnu/libhiredis.a -pthread
.PHONY:clean
clean:
rm -f hello
rm -f generic
补充:
在set的参数,关于key的类型是一个StringView,它跟string类型不同的就是,string既可以读还可以修改,StringView只能读,不能修改。
用StringView的好处就是效率比std::string更高。
另外关于get函数的返回值
这是一个OptionalString类型。
这个类型可以表示无效值,因为std::string和string*都不方便表示无效值,因此redis-plus-plus作者自己封装了一个这样的类型。并在在C++14版本中也引入了optional类型。
这个类型在这里并不支持 移位运算符的重载,我们可以通过如下的方式来获取它的string内容,再进行打印
如果这个 optional是一个无效值,那么当我们试图获取它的string对象时就会抛出一个异常。
然而C++的异常其实并不好用。
我们可以用if判定的方式来代替异常。
另外,像我们这样进行单元测试的时候,像flushall这样清楚数据的操作放在函数开头比较好。
exists / del
这两个函数就很简单了。
exists
void test2(sw::redis::Redis& redis)
{
cout << "exists " << endl;
redis.flushall();
redis.set("key1","11");
redis.set("key2","22");
redis.set("key3","33");
auto ret = redis.exists("key1");
cout << ret << endl;
ret = redis.exists("key5"); // 不存在
cout << ret << endl;
ret = redis.exists({"key1","key2","key3","key5"});
cout << ret << endl;
}
exists存在重载,可以传一个初始化列表,这样可以检测多个key。
执行结果:
del
void test3(sw::redis::Redis& redis)
{
cout << "del " << endl;
redis.flushall();
redis.set("key1","11");
redis.set("key2","22");
redis.set("key3","33");
auto ret = redis.del("key1");
cout << ret << endl;
ret = redis.del("key5"); // 不存在
cout << ret << endl;
}
执行结果
keys
在使用keys命令前,我们需要先了解什么是插入迭代器,也就是keys命令的返回值类型。
STL中有物种迭代器。我们之前有用过sort,就是用的随机访问迭代器。
插入迭代器本质也是一种 输出迭代器。
通常一个迭代器主要是表示一个 位置。
而插入迭代器则是 位置 + 动作。
比如刚刚的 back_insert_iterator。不过像这样的迭代器构造函数写起来比较麻烦,于是一般会使用一些辅助的函数来进行构造。
对于插入迭代器来说,最核心的操作就是 赋值运算符的重载 = 。
比如it是一个插入迭代器(比如 back_insert_iterator),然后it2是一个普通迭代器,那么it = it2的操作,就相当于获取到it2指向的元素,然后把这个元素插入到it指向的容器末尾。
为什么要这样设计迭代器呢?
主要还是为了解耦合,比如keys命令这里它看不见容器,容器也看不见keys,它们是以迭代器为中间介质运作的。
代码:
void test4(sw::redis::Redis& redis) {
std::cout << "keys" << std::endl;
redis.flushall();
redis.set("key", "111");
redis.set("key2", "222");
redis.set("key3", "333");
redis.set("key4", "444");
redis.set("key5", "555");
redis.set("key6", "666");
// keys 的第二个参数, 是一个 "插入迭代器". 咱们需要先准备好一个保存结果的容器.
// 接下来再创建一个插入迭代器指向容器的位置. 就可以把 keys 获取到的结果依次通过刚才的插入迭代器插入到容器的指定位置中了.
std::vector<std::string> result;
auto it = std::back_inserter(result);
redis.keys("*", it);
printContainer(result);
}
可见这里的it,我们用的back_inserter来构造了一个插入迭代器,用一个vector<string>容器构造的
另外对于打印这个迭代器的内容,专门封装了一个函数
template<typename T>
inline void printContainer(const T& container) {
for (const auto& elem : container) {
std::cout << elem << std::endl;
}
}
expire / ttl
在expire中,
我们需要传入一个超时时间,而这个时间的类型是std::chrono::seconds,在C++11中,我们可以直接使用字面值常量来传参。
代码:
void test5(sw::redis::Redis& redis)
{
using namespace std::chrono_literals; // 包含之后就可以使用字面值常量了(C++11)
cout << "expire and ttl" << endl;
redis.flushall();
redis.set("key1","11");
// 10s => std::chrono::seconds(10)
redis.expire("key1",10s);
std::this_thread::sleep_for(2s);
auto ret = redis.ttl("key1");
cout << ret << endl;
}
执行结果
type
代码:
void test6(sw::redis::Redis& redis)
{
cout << "type " << endl;
redis.flushall();
redis.set("key1","11");
auto ret = redis.type("key1");
cout << ret << endl;
redis.hset("key2","jkm","666");
ret = redis.type("key2");
cout << ret << endl;
}
执行结果
到这里我们发现redis-plus-plus提供的各种函数,跟我们之前学过的redis命令是相匹配的。
string类型
刚刚对于一些简单的命令的介绍主要是为了熟悉C++的一些语法,毕竟命令使用起来都大差不差,并且像 back_insert_iterator 或者OptionalString类型这样的用法熟悉之后,就可以针对类型来了解命令了。
get / set
代码:
void test1(sw::redis::Redis& redis)
{
cout << "get and set" << endl;
redis.flushall();
redis.set("key1","11");
redis.set("key2","22");
auto ret = redis.get("key1"); // 这里的返回值类型是sw::redis::OptionalString
if(ret)
{
cout << ret.value() << endl;
}
ret = redis.get("key2");
if(ret)
{
cout << ret.value() << endl;
}
}
set 带超时时间的版本
void test2(sw::redis::Redis& redis)
{
cout << "set 带有超时时间版本的使用" << endl;
redis.flushall();
//redis.set("key1","11",std::chrono::seconds(10));
redis.set("key1","11",10s);
std::this_thread::sleep_for(2s); // 或者 2000ms
auto ret = redis.ttl("key1"); // long long
cout << ret << endl;
}
set NX 和 XX
如果单用一个set,那么需要搭配过期时间的版本来使用
void test3(sw::redis::Redis& redis)
{
cout << "set 的NX 和 XX " << endl;
redis.flushall();
// set 的重载版本中, 没有单独提供 NX 和 XX 的版本, 必须搭配过期时间的版本来使用.
redis.set("key1","11",0s,sw::redis::UpdateType::EXIST);
auto ret = redis.get("key1");
if(ret)
{
cout << ret.value() << endl;
}
else
{
cout << "设置出错" << endl;
}
}
mset
mset有两个重载版本
1.使用初始化列表,里面用pair的形式设置 key val
2.使用容器迭代器的方式
通过传入容器的begin和end来进行初始化
void test4(sw::redis::Redis& redis)
{
cout << "mset" << endl;
redis.flushall();
// 使用初始化列表描述多个键值对
//redis.mset({std::make_pair("key1","11"),std::make_pair("key2","22")});
// 把多个键值对提前组织到容器中. 以迭代器的形式告诉 mset
std::vector<std::pair<std::string,std::string>> keys = {
{"key1","11"},
{"key2","22"}
};
redis.mset(keys.begin(),keys.end());
auto ret = redis.get("key2");
if(ret)
{
cout << ret.value() << endl;
}
}
mget
void test5(sw::redis::Redis& redis)
{
cout << "mget" << endl;
redis.flushall();
redis.mset({std::make_pair("key1","11"),std::make_pair("key2","22"),std::make_pair("key3","33")});
std::vector<sw::redis::OptionalString> res;
auto it = std::back_inserter(res);
redis.mget({"key1","key2","key3"},it);
printContainer(res);
}
对于std::vector<sw::redis::OptionalString> 专门重载了一下printContainer()
// 函数重载实现特化
inline void printContainer(const std::vector<sw::redis::OptionalString>& container) {
for (const auto& elem : container) {
std::cout << elem.value() << std::endl;
}
}
getrange / setrange
void test6(sw::redis::Redis& redis)
{
cout << "getrange and setrange" << endl;
redis.flushall();
redis.set("key1","jkMMdaisuki");
std::string ret = redis.getrange("key1",0,-1); // 这里返回值就是string,可以直接打印
cout << ret << endl;
redis.setrange("key1",2,"JJ");
ret = redis.getrange("key1",0,-1); // 这里返回值就是string,可以直接打印
cout << ret << endl;
}
incr / decr
void test7(sw::redis::Redis& redis)
{
cout << "incr and decr" << endl;
redis.flushall();
redis.set("key1","100");
long long ret = redis.incr("key1"); // long long
cout << ret << endl;
ret = redis.decr("key1");
cout << ret << endl;
}
list类型
lpush / lrange
可以用初始化列表的方式来插入多个val
void test1(sw::redis::Redis& redis)
{
cout << "lpush and lrange" << endl;
redis.flushall();
// 插入单个元素
redis.lpush("key", "111");
// 插入一组元素, 基于初始化列表
redis.lpush("key",{"1","2","3","4"});
// 插入一组元素, 基于迭代器
vector<string> values = {"555", "666", "777"};
redis.lpush("key", values.begin(), values.end());
// lrange 获取到列表中的元素
vector<string> res;
auto it = std::back_inserter(res);
redis.lrange("key",0,-1,it);
printContainer(res);
}
rpush
用法与lpush一致,不多介绍
void test2(sw::redis::Redis& redis)
{
cout << "rpush " << endl;
redis.flushall();
// 用法跟lpush一致
// 插入一组元素, 基于初始化列表
redis.rpush("key",{"1","2","3","4"});
// lrange 获取到列表中的元素
vector<string> res;
auto it = std::back_inserter(res);
redis.lrange("key",0,-1,it);
printContainer(res);
}
lpop / rpop
void test3(sw::redis::Redis& redis)
{
cout << "lpop and rpop" << endl;
redis.flushall();
// 插入一组元素, 基于初始化列表
redis.rpush("key1",{"1","2","3","4"});
auto ret = redis.lpop("key1");
if(ret)
{
cout << "lpop : " << ret.value() << endl;
}
ret = redis.rpop("key1");
if(ret)
{
cout << "rpop : " << ret.value() << endl;
}
}
blpop
阻塞版本
void test4(sw::redis::Redis& redis)
{
cout << "blpop" << endl;
redis.flushall();
// blpop 的返回值是sw::redis::OptionalStringPair,这是一个pair结构的
auto ret = redis.blpop("key1"); // 不写timeout,默认为0表示阻塞等待
if(ret)
{
cout << "key : " << ret->first << endl;
cout << "emlm : " << ret->second << endl;
}
else
{
cout << "等待超时" << endl;
}
}
当没有元素时:
阻塞住了。
另起一个客户端进行插入一个元素时
客户端立马就把元素删除了。
llen
void test5(sw::redis::Redis& redis)
{
cout << "llen" << endl;
redis.flushall();
// 插入一组元素, 基于初始化列表
redis.rpush("key1",{"1","2","3","4"});
long long ret = redis.llen("key1"); // long long
cout << ret << endl;
}
set类型
sadd / smembers
void test1(sw::redis::Redis& redis)
{
cout << "sadd and smembers" << endl;
redis.flushall();
// 一次插入一个值
redis.sadd("key","11");
// 初始化列表一次插入多个值
redis.sadd("key",{"22","33","44"});
// 容器迭代器的方式
set<string> elems = {"55","66"};
redis.sadd("key",elems.begin(),elems.end());
// 获取所有元素 smembers
// 此处用来保存 smembers 的结果, 使用 set 可能更合适.
set<string> res;
// 由于此处 set 里的元素顺序是固定的. 指定一个 result.end() 或者 result.begin() 或者其他位置的迭代器, 都无所谓
auto it = std::inserter(res,res.end()); // 这里就用的是inserter函数构造迭代器了
redis.smembers("key",it);
printContainer(res);
}
如果我们用set容器调用std::back_inserter来构造插入迭代器(输出迭代器)的话,是会报错的
因为在set容器中没有push_back方法。
sismember
void test2(sw::redis::Redis& redis)
{
cout << "sismember" << endl;
redis.flushall();
// 初始化列表一次插入多个值
redis.sadd("key",{"22","33","44"});
bool ret = redis.sismember("key","33");
cout << "ret : " << ret << endl;
}
scard
void test3(sw::redis::Redis& redis)
{
cout << "scard" << endl;
redis.flushall();
// 初始化列表一次插入多个值
redis.sadd("key",{"22","33","44"});
long long len = redis.scard("key");
cout << "len : " << len << endl;
}
spop
void test4(sw::redis::Redis& redis)
{
cout << "spop" << endl;
redis.flushall();
// 初始化列表一次插入多个值
redis.sadd("key",{"22","33","44"});
auto ret = redis.spop("key"); // 返回值sw::redis::OptionalString,因为有可能key不存在
if(ret)
{
cout << "ret : " << ret.value() << endl;
}
}
注意 spop是随机删除的,所以我这里的执行结果每次都可能不一样。
sinter
void test5(sw::redis::Redis& redis)
{
cout << "sinter" << endl;
redis.flushall();
redis.sadd("key1",{"11","22","33"});
redis.sadd("key2",{"22","33","44"});
set<string> res;
auto it = std::inserter(res,res.end());
redis.sinter({"key1","key2"},it);
printContainer(res);
}
sinterstore
void test6(sw::redis::Redis& redis)
{
cout << "sinterstore" << endl;
redis.flushall();
redis.sadd("key1",{"11","22","33"});
redis.sadd("key2",{"22","33","44"});
redis.sinterstore("key3",{"key1","key2"});
// 获取key3的元素
set<string> res;
auto it = std::inserter(res,res.end());
redis.smembers("key3",it);
printContainer(res);
}
hash类型
hset / hget
void test1(sw::redis::Redis& redis)
{
cout << "hset and hget" << endl;
redis.flushall();
// 一次性插入一个值
redis.hset("key","f1","11");
redis.hset("key",std::make_pair("f2","22"));
// 使用初始化列表
redis.hset("key",{std::make_pair("f3","33"),std::make_pair("f4","44")});
// 使用容器
vector<std::pair<string,string>> fields = {
std::make_pair("f5","55"),
std::make_pair("f6","66")
};
redis.hset("key",fields.begin(),fields.end());
// 获取元素
auto ret = redis.hget("key","f2");
if(ret)
{
cout << "ret: " << ret.value() << endl;
}
}
hexits
void test2(sw::redis::Redis& redis)
{
cout << "hexists" << endl;
redis.flushall();
// 使用初始化列表
redis.hset("key",{std::make_pair("f3","33"),std::make_pair("f4","44")});
bool ret = redis.hexists("key","f3");
cout << "ret: " << ret << endl;
}
hdel
void test3(sw::redis::Redis& redis)
{
cout << "del" << endl;
redis.flushall();
// 使用初始化列表
redis.hset("key",{std::make_pair("f3","33"),std::make_pair("f4","44"),std::make_pair("f5","55")});
long long ret = redis.hdel("key","f3");
cout << "ret: " << ret << endl;
ret = redis.hdel("key",{"f4","f5"});
cout << "ret: " << ret << endl;
}
hkeys / hvals
void test4(sw::redis::Redis& redis)
{
cout << "hkeys and hvals" << endl;
redis.flushall();
// 使用初始化列表
redis.hset("key",{std::make_pair("f3","33"),std::make_pair("f4","44"),std::make_pair("f5","55")});
vector<string> resKeys;
auto itKesy = std::back_inserter(resKeys);
redis.hkeys("key",itKesy);
printContainer(resKeys);
vector<string> resVals;
auto itVals = std::back_inserter(resVals);
redis.hvals("key",itVals);
printContainer(resVals);
}
hmset / hmget
void test5(sw::redis::Redis& redis)
{
cout << "hmset and hmget" << endl;
redis.flushall();
redis.hset("key",{
std::make_pair("f3","33"),std::make_pair("f4","44"),
std::make_pair("f5","55")
}); // 用法和set差不多
vector<string> res;
auto it = std::back_inserter(res);
redis.hmget("key",{"f3","f4","f5"},it);
printContainer(res);
}
zset类型
zadd / zrange
void test1(sw::redis::Redis& redis)
{
cout << "zadd and zrange" << endl;
redis.flushall();
redis.zadd("key","亚索",99.5);
redis.zadd("key","狐臭",0.5);
redis.zadd("key","阿狸",100);
// 还有容器插入的方式,这里就不多介绍了
// zrange 支持两种主要的风格:
// 1. 只查询 member, 不带 score
// 2. 查询 member 同时带 score
// 关键就是看插入迭代器指向的容器的类型.
// 指向的容器只是包含一个 string, 就是只查询 member
// 指向的容器包含的是一个 pair, 里面有 string 和 double, 就是查询 member 同时带有 score
vector<string> members;
auto it = std::back_inserter(members);
redis.zrange("key",0,-1,it);
printContainer(members);
vector<std::pair<string,double>> membersWithCores;
auto it2 = std::back_inserter(membersWithCores);
redis.zrange("key",0,-1,it2); // 如果迭代器的元素是pair类型的,就会把分数也带上
printContainer(membersWithCores);
}
另外针对pair类型的容器打印
inline void printContainer(const std::vector<std::pair<std::string,double>>& container)
{
for(const auto& p : container)
{
std::cout << p.first << " " << p.second << std::endl;
}
}
zcard
void test2(sw::redis::Redis& redis)
{
cout << "zcard" << endl;
redis.flushall();
redis.zadd("key","亚索",99.5);
redis.zadd("key","狐臭",0.5);
redis.zadd("key","阿狸",100);
long long ret = redis.zcard("key");
cout << "ret: " << ret << endl;
}
zrem
void test3(sw::redis::Redis& redis)
{
cout << "zrem" << endl;
redis.flushall();
redis.zadd("key","亚索",99.5);
redis.zadd("key","狐臭",0.5);
redis.zadd("key","阿狸",100);
long long ret = redis.zrem("key","狐臭"); // 也可以删除多个
cout << "ret: " << ret << endl;
}
zscore
void test4(sw::redis::Redis& redis)
{
cout << "zscore" << endl;
redis.flushall();
redis.zadd("key","亚索",99.5);
redis.zadd("key","狐臭",0.5);
redis.zadd("key","阿狸",100);
auto ret = redis.zscore("key","阿狸");
if(ret)
{
cout << "ret: " << ret.value() << endl;
}
else
{
cout << "该值不存在" << endl;
}
}
zrank
void test5(sw::redis::Redis& redis)
{
cout << "zrank" << endl;
redis.flushall();
redis.zadd("key","亚索",99.5);
redis.zadd("key","狐臭",0.5);
redis.zadd("key","阿狸",100);
auto ret = redis.zrank("key","阿狸"); // 返回值是sw::redis::OptionalLongLong
if(ret)
{
cout << "ret: " << ret.value() << endl;
}
else
{
cout << "该值不存在" << endl;
}
}
这就是redis-plus-plus的一些基本的使用方法。