Redis简介:内存数据库的核心概念与经典应用场景 我们先从几个核心概念说起。相比MySQL,Redis也可以当作数据库来用,而且快得多——因为它把所有数据都塞在内存里。但内存空间远小于硬盘,所以存储量有限,功能也比较“克制”,比如不支持多表查询这类操作。 这就引出了经典的“二八原则”:20%的数据
详解Redis终端操作与Redis-plus-plus接口使用教程
时间:2026-07-07
浏览:546
文章目录
Redis简介:内存数据库的核心概念与经典应用场景
我们先从几个核心概念说起。相比MySQL,Redis也可以当作数据库来用,而且快得多——因为它把所有数据都塞在内存里。但内存空间远小于硬盘,所以存储量有限,功能也比较“克制”,比如不支持多表查询这类操作。

这就引出了经典的“二八原则”:20%的数据扛起了80%的访问需求。典型的做法是Redis搭配MySQL,用Redis缓存热点数据,剩下的数据老老实实放在MySQL里,只在需要时去取。当然,这样做的代价是系统复杂度上去了,还得解决Redis和MySQL之间的数据同步问题。
说到Redis的初衷,它最初是想当消息队列用的(也就是分布式系统里的生产者-消费者模型)。另外,MySQL的主从分离和读写分离大家也熟悉:读请求远多于写请求,所以只有主库处理写操作,然后把变更同步给从库,从库只负责读。上层再搞个负载均衡,把读请求分散到各个从节点上。
Redis存放、获取键值的操作
1. 存放键值对
在Redis里,key始终是字符串类型,这点得记牢。存放键值对直接用set keyname keyvalue就好,Redis不区分大小写,也不严格要求引号——写不写都行。
更多玩法:set keyname value [ex seconds | px milliseconds] [nx|xx]。ex指定超时时间(秒),px是毫秒;nx表示键不存在才插入,xx表示键存在时才修改。
批量操作:mset keyname value [keyname value],一次设置多组键值对。
还有几个快捷指令:setnx(不存在才设)、setex(设过期时间,秒)、psetex(设过期时间,毫秒)。
2. 获取键值对
取值用get keyname,批量取值用mget。
获取键值对时的匹配原则
匹配任意单个字符,比如ke能找出key。*匹配任意多个字符,用法和Linux文件通配符一样。[abcdefg]匹配中括号中的任一字符。[^e]排除字符e。[a-c]范围匹配。
不过,实际开发中几乎没人用*来扫全库的key——因为Redis是单线程的,这么做会导致线程卡在检索上,大量请求拿不到缓存数据,一股脑涌向数据库,数据库很容易被压垮。
3. 查看键值对是否存在
用exists keyname1 keyname2 ...一次检查多个key是否存在。为什么Redis支持一次带多个key?因为Redis是客户端-服务器架构,每次请求都要走网络,网络传输和协议封装解包很耗时,所以干脆一次多带几个,省掉多次往返的延迟。
4. 删除键值对
删除用del keyname1 keyname2。
5. 设置过期时间
expire keyname seconds– 秒为单位。pexpire keyname microseconds– 毫秒为单位。
6. 查看存活时间
ttl keyname,返回剩余存活时间。没设过期时间返回-1,key不存在返回-2。
Redis怎样删除已经过期的key?
Redis采用两种策略结合:定期删除和惰性删除。定期删除会定时抽一小部分key检查,因为Redis是单线程,不能花太多时间在这里,否则会阻塞。惰性删除则是在访问某个key时,发现它过期了,顺手就删掉。它没有用定时器来处理过期——不过定时器方案本身也值得了解,比如优先级队列和时间轮。
优先级队列
把有过期时间的任务按过期时间建成小根堆,线程根据堆顶的过期时间休眠,到点再唤醒。如果中间有新任务进来,就唤醒线程重新计算。
时间轮
用一个环形队列,每个格子指向一个链表头,链表里的任务过期时间落在某个范围内。检测线程定期走一格,处理该格中到期的任务。环形和周期叠加,一个链表中可能有不同时间的任务。时间轮对时间粒度和队列长度设计要求比较高。
7. 查看类型
type keyname 能查看key对应value的数据类型。
Redis数据类型与实现方式
Redis对外承诺了多种数据类型,但内部实现可能偷偷换了马甲。
1. string
raw– 普通字符串存储。int– 如果字符串是数字,直接按整数存。embstr– 对短字符串做了优化,也能存小数,但整数比较时就会慢一点,因为得先转成数字再比。
2. hash
hashtable– Redis自己实现的哈希表。ziplist– 压缩列表。当哈希表元素很少时,用ziplist遍历也快,而且内存更紧凑。元素多了效率就下来了。
3. list
老版本用linkedlist + ziplist,Redis 3.2以后改为quicklist,类似C++的deque。
4. set
hashtable– 哈希表。intset– 整数集合,数据少且全是整数时使用。
5. zset
skiplist– 跳表,每个节点有多个指针域,能实现O(log N)的查询。ziplist– 元素少或元素体积小时使用。
查看具体存储方式
用object encoding keyname 就能看到某个value实际采用了哪种底层结构。
Redis的单线程工作模型
客户端发请求是并发的,但Redis服务器处理核心逻辑是单线程的,所有请求串行执行,没有并发问题。所以如果一个操作阻塞太久,可能整个服务就挂了。反过来讲,Redis处理的业务都是短平快的,对并行需求很低,单线程完全够用。
为什么说Redis快,效率高?
首先,Redis快主要是和MySQL这类磁盘数据库比。内存操作比磁盘操作快好几个数量级,先天优势。其次,Redis功能精简,没有约束检查、多表联查这些复杂操作,省掉了大量额外开销。
string 类型
1. incr和incrby
incr– 给value加1,只有int类型才能用。incrby– 加n,n可以是负数。key不存在就从0开始算。
2. decr和decrby
和incr系列相反,做减法。
3. incrbyfloat
支持小数加减,想减就传负数。
4. append
追加字符串,返回追加后的长度。注意:Redis不会对字符集做特殊处理,输入中文默认按原编码存储。可以在启动客户端时加--raw参数,让客户端试着把二进制数据转成可读的中文。
5. getrange
getrange keyname start end 取[start, end]区间的字节内容。因为按字节操作,中文可能被截成乱码(UTF-8或GBK一个汉字占多个字节)。end可以是负数,-1代表最后一个字符。如果start>end会取不到内容。
6. setrange
setrange key start value,从start位置开始覆盖为value,覆盖长度取决于value的长度。如果key不存在,会用空格填充到start位置。如果start超过了原内容长度,中间也用空格补齐。
7. strlen
返回字符串value的字节长度,中文按实际字节数算。
Redis中的hash类型
Redis的hash结构是field-value对,value只能是字符串。
1. hset、hget、hexists、hdel
hset keyname fieldname value– 设置哈希表中的字段。hget keyname fieldname– 获取字段值。hexists keyname fieldname– 判断字段是否存在。hdel keyname fieldname– 删除字段。
2. hkeys、hvals
hkeys key获取所有field,hvals key获取所有value。慎用这两个命令,跟keys *一样可能阻塞Redis。
hlen、hsetnx、hincrby、hincrbyfloat
hlen keyname– 哈希表的大小。hsetnx– 不存在则插入,存在则失败。hincrby– 给某个value加n(数字类型)。hincrbyfloat– 加小数。
使用哈希存储的场景
可以把哈希当作“对象”用,存储一个实体的多个属性,省去大量独立的key。更新某个字段时不用把整个数据读出来再写回去。另外,哈希有稀疏性——不需要的字段可以不存,而关系型数据库里即使没有值也得用NULL占位。
列表
列表类似顺序表,但底层用类似deque的容器,在头尾插入删除效率高。列表是有序的:元素相同但顺序不同就是两个不同的列表。
lpush lpop rpush rpop
分别对应push_front、pop_front、push_back、pop_back。
lrem lindex lrange
lrem key count value– 删除列表中前count个等于value的元素。lrange key start stop– 返回[start, stop]区间的元素(显示结果时前面的数字是序号,不是下标)。lindex key index– 获取指定下标的元素。
lpushx 和 rpushx
只有列表存在时才插入,带x表示exist。
lpop 和 rpop
lpop keyname [count] 从左端删除count个(低版本可能不支持count参数),rpop同理。搭配lpush/rpush可以当栈或队列用。
linsert
linsert keyname [before|after] pivot value,在pivot值的前或后插入value,pivot从左到右找第一个。
lrem
lrem keyname count element:count>0从左到右删count个;count=0删所有等于element的;count<0从右到左删|count|个。
ltrim 和 lset
ltrim keyname start stop– 只保留[start, stop]区间,其余丢弃。lset keyname index element– 将index位置设为element,超出范围会报错。
blpop 和 brpop
阻塞等待。Redis的阻塞是特殊的,不会影响核心逻辑。不会考虑队列满的情况,只考虑队列为空时阻塞等待。blpop keyname timeout,timeout=0表示一直等。可以同时等多个队列,从左到右第一个有数据的返回。
list列表的应用场景
时间轴、消息轴:用lpush插新内容,lrange分页,ltrim保留部分。消息队列:lpush + brpop就能搭一个阻塞队列。
set集合
set和list不同:set无序,且元素不能重复。
sadd smember sismember scard
sadd key elements– 添加元素。smembers key– 获取所有元素。sismember key member– 判断member是否在集合中。scard keyname– 获取元素个数。
spop srandmember
spop keyname– 随机弹出一个元素并删除。srandmember keyname– 随机返回一个元素,但不删除。
srem smove
srem keyname element– 删除指定元素。smove source destination element– 把source中的element移到destination中。如果destination已存在该元素,操作成功但不会重复。
sinter sinternstore 交集
sinter set1 set2 ...– 求交集。sinterstore dest set1 set2 ...– 求交集并存入dest。
sunion sunionstore sdiff sdiffstore
并集和差集,用法与交集相同。
set的应用场景
用户画像:用标签(小字符串)打标签。好友推荐:set求交集很方便。UV统计:UV需要去重,set正合适(PV是Page View,统计访问次数;UV是User View,统计独立用户数)。
zset
zset也是有序的,但它的“有序”是真正的排序——基于score。每个member关联一个score,类似pair。
zadd
zadd key [nx|xx] [gt|lt] [ch] [incr] score member [score member ...]
- 默认返回新增元素个数;如果加了
ch,返回被修改的元素个数。 - gt/lt:当新score大于/小于原score时才更新。
- nx/xx:只增/只改。
- incr:像incrby一样操作score。
zrange
zrange key start end [withscore]– 展示元素。zrevrange key start end [withscore]– 逆序展示。zrangebyscore key start end [withscore]– 按score范围展示,start/end是score区间。
zcount
zcount key start end – 统计score区间内的member个数。用(表示开区间,比如(2 (5,支持-inf和inf。
zcard
zcard key – 统计member个数。
zpopmax
zset默认升序排列,zpopmax会删除score最大的member并返回。可加count一次取多个,时间复杂度O(log N * M)。
bzpopmax
bzpopmax key [key...] timeout – 阻塞式取最大member。可同时等多个key,任一有数据就返回。
zpopmin bzpopmin
和zpopmax/bzpopmax相反,操作最小值。
zrank zrevrank zscore
zrank key member– 查询member从前往后的排名(类似数组下标)。zrevrank key member– 从后往前的排名(最后一个元素下标为0)。zscore key member– 查分数,Redis用空间换时间,这个操作是O(1)。
zrem zremrangebyrank zremrangebyscore
zrem key member– 删除member,O(log N)。zremrangebyrank key start end– 删除排名区间内的member。zremrangebyscore key min max– 删除score区间内的member,支持(开区间。
zincrby
zincrby key increment member – 给member的score增加increment(可为负)。
zinterstore
zinterstore dest keynum key [key...] weights [weight...] [aggregate sum | min | max]
- dest存交集结果,keynum指定参与求交的zset个数。
- weights设置权重,与member的score相乘后按aggregate策略合并(sum/min/max)。
- 最坏时间复杂度O(N*K) + O(M*log M),N为最小集合元素数,K为集合数,M为交集元素数。
zunionstore
用法类似zinterstore,求并集。时间复杂度O(N) + O(M*log M),N是总元素数,M是并集元素数。
zset的应用场景
排行榜系统:微博热搜、游戏天梯、成绩排名。加权排行:维护多个zset,用zinterstore/zunionstore按权重合并。
其他类型
- stream – 阻塞队列,让Redis当消息队列用。
- geospatial – 存经纬度坐标,按半径或矩形区域查找。
- hyperloglog – 估算集合元素个数,最多12KB空间,误差约0.8%,适合对精度要求不高的UV/PV统计。
- bitmap – 优化过的set,省空间但只支持整数。
- bitfield – 位域,类似C语言位段,可以对字节中的位做读写计算,目标是省空间。
渐进式遍历
传统keys *一次扫全库容易阻塞,渐进式遍历一次只查一小部分,避免卡住。scan cursor [MATCH match] [COUNT count] [TYPE type]。cursor不是下标,而是服务器返回的下次遍历的开始位置。输入0开始,返回0表示遍历结束。match支持通配符,count默认10。注意:scan不保留状态,中途可以停,但遍历过程中如果数据发生变化,结果可能不准确。
Redis数据库操作
Redis也有“数据库”的概念,不过比MySQL弱。默认有16个库,不能增删,默认操作在0号库。select index切换库(0-15)。dbsize看当前库key数。flushdb [async|sync]清空当前库(异步或同步),flushall清空所有库。
Redis自定义客户端编写
RESP协议
Redis客户端和服务器之间用RESP协议通信,基于TCP,一问一答。客户端用Bulk String数组发送命令,服务器根据命令返回不同格式。
协议格式
+OK\r\n
Simple String,以+开头
-ERR\r\n
Error,以-开头
:1000\r\n
整数,以:开头
$5\r\nhello\r\n
Bulk String,以$开头,可传二进制数据
*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
Array,以*开头
Redis-plus-plus
声明连接对象:
#includesw::redis::Redis redis("tcp://127.0.0.1:6379");
然后可以通过ping检查连通性。
String
set、get方法:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
using namespace std::chrono_literals;
redis.set("key1", "val1", 0s, sw::redis::UpdateType::NO_EXIST);
auto ret = redis.get("key1");
if(ret) std::cout << ret.value() << std::endl;
set方法用sw::redis::StringView作为key和value,高效只读。返回值是sw::redis::OptionalString,可隐式转bool判断是否存在,有效时用value()取出。第三个参数是过期时间(std::chrono::milliseconds),第四是更新策略:EXIST对应setxx,NOT_EXIST对应setnx,ALWAYS就是普通set。
exists和del:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
auto ret = redis.exists("key1");
ret = redis.del({"key1", "key2"});
keys:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
vector ret;
auto it = std::back_inserter(ret);
redis.keys("*", it);
expire和ttl:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
using namespace std::chrono_literals;
bool ret = redis.expire("key", 10s);
std::this_thread::sleep_for(5s);
long long len = redis.ttl("key");
type:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
redis.set("key", "val");
std::string ret = redis.type("key");
mget/mset:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
redis.mset({std::make_pair("key1", "val1"), std::make_pair("key2", "val2")});
std::vector<:string> keys;
redis.mset(keys.begin(), keys.end());
std::vector<:redis::optionalstring> vals;
auto bit = std::back_inserter(vals);
redis.mget({"key1", "key2"}, bit);
std::vector<:string> vals2 = {"key1", "key2"};
redis.mget(vals2.begin(), vals2.end(), bit);
getrange/setrange:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
std::string ret = redis.getrange("key", 5, 10);
long long val = redis.setrange("key", 5, "ins_val");
incr/decr:
sw::redis::Redis redis("tcp://127.0.0.1:6379");
long long ret1 = redis.incr("key");
long long ret2 = redis.decr("key");
list
lpush/lrange/rpush/rpop/lpop:
sw::redis::Redis server("tcp://127.0.0.1:6379");
server.lpush("list1", "1");
server.lpush("list1", {"1", "2", "3"});
std::vector<:string> vals = "1", "2", "3";
server.lpush("list1", vals.begin(), vals.end());
std::vector<:string> rets;
auto bit = std::back_inserter(rets);
server.lrange("list1", bit);
auto ret1 = server.rpop("list1");
auto ret2 = server.lpop();
brpop/blpop:
sw::redis::Redis server("tcp://127.0.0.1:6379");
auto ret = server.blpop("list1", std::chrono::seconds(1));
if(ret){
std::cout << ret.value().first << ret.value().second << std::endl;
}
llen:
sw::redis::Redis server("tcp://127.0.0.1:6379");
long long server.llen("list");
Set
sadd/smembers:
sw::redis::Redis server("tcp://127.0.0.1:6379");
server.sadd("set", "1");
set mems;
auto in = std::inserter(mems, mems.end());
server.smembers("set", in);
sismember/spop/scard:
sw::redis::Redis server("tcp://127.0.0.1:6379");
bool exists = server.ismember("set", "1");
auto ret = spop("set");
if(ret) std::cout << ret.value() << std::endl;
long long num = scard("set");
Hash
hget/hset:
sw::redis::Redis server("tcp://127.0.0.1:6379");
long long ret = server.hset("hash1", "1", "zhangsan");
ret = server.hset("hash1", {std::make_pair("2", "lisi"), std::make_pair("3", "wangwu")});
auto val = server.hget("hash1", "1");
if(val) std::cout << val.value() << std::endl;
hexists/hdel/hlen:
sw::redis::Redis server("tcp://127.0.0.1:6379");
bool exists = server.hexists("hash1", "1");
long long ret = server.hdel("hash1", {"1", "2"});
ret = server.hlen("hash1");
hmset/hmget/hkeys/hvals:
std::vector<:string> keys;
auto it = std::back_inserter(keys);
server.hkeys("hash1", it);
std::vector<:string> vals;
it = std::back_inserter(vals);
server.hvals("hash1", it);
server.hmget("hash1", {"3", "4", "5"}, it);
ZSet
zadd/zrange:
server.zadd("zset1", "zhangsan", 1);
server.zadd("zset1", {std::make_pair("lisi", 2), std::make_pair("wangwu", 3)});
std::vector<:pair std::string>> pairs = {
std::make_pair("zhouliu", "4"),
std::make_pair("tianqi", "5")
};
server.zadd("zset1", pairs.begin(), pairs.end());
std::vector<:string> withnoscore;
auto it = std::back_inserter(withnoscore);
std::vector<:pair double>> withscore;
auto it2 = std::back_inserter(withscore);
server.zrange("zset1", 0, -1, it2);
zcard/zscore/zrank/zrem:
long long ret = server.zcard("zset1");
auto score = server.zscore("zset1", "tianqi");
auto rank = server.zrank("zset1", "zhangsan");
server.zrem("zset1", "lisi");实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。