kekobin / blog

blog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

redis cli命令

kekobin opened this issue · comments

Redis 是一个免费开源、基于内存的高性能 Key-Value 数据库

mac redis安装

参考

为什么要使用 Redis?都有哪些好处?

Redis 具有速度快、数据持久化、丰富的数据类型、多语言支持和多功能等特性

速度快

Redis 号称具有 10W OPS(每秒10万次读写)的能力,Redis 是基于单线程模型,将数据存在于内存中,采用 C 语言(距操作系统最近的语言) 50000 行代码(单机版的 23000 行)编写。

为什么单线程模型如此之快?取决于几个优点:基于内存、非阻塞IO、避免线程切换,这点和 Node.js 很相似,但是单线程需要注意由于一次只能运行一次命令,使用过程中要拒绝慢命令,例如 keys、flushall、flushdb、slow lua script、mutil/exec 等

持久化

Redis 之所以快还有一个原因是其基于内存模型,内存中进行数据存储一个典型的问题是断电后或者服务重启会造成数据丢失,那么针对这个问题 Redis 中提出了两种数据持久化策略,分别为 RDB 和 AOF 会将 Redis 在内存中保存的数据异步更新到磁盘中,实现数据的持久化功能

  • 原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。比如A、B函数一起执行,这个就是非原子的,因为A成功B是可能失败的

Redis 与 Memcache 的区别?

1.数据持久化
Memcache 仅存于内存中不支持数据持久化,这样如果断电或服务重启都会造成数据丢失,而 Redis 提供了数据持久化的能力,可以分别通过 RDB 或 AOF 两种策略将数据持久化在硬盘中,服务重启或断电也不会造成数据丢失。

2.数据类型
Memcache 对数据类型的支持相对要简单、过于单一,Redis 的数据类型就显得很丰富,支持 String、HashTable、List、Set、Zset 还有最新的 BitMaps、HyperLogLog 等

3.Value 大小
在 Redis 中 Value 可以是字符串、二进制数据,例如,您可以在 Value 内存储 jpeg 图像,但是建议值不要大于 512 MB。Memcache 的最大 key 长度为 250 个字符,可以接受的 Value 存储数据大小为 1MB,这也是典型的 slab 最大值。

4.数据一致性
Redis 基于单线程模型,保证了顺序一致性问题,如果有多个命令需要一起操作,也可以使用事物或者编写 Lua 脚本。Memcache 需要借助 CAS(Compare And Set)保证数据一致性,简单的理解就是在进行 set 操作时,加上初始值 Compare,如果初始值发生改变则不允许 set。

  • 这也解答了多个node请求过来,执行log不会串的问题

redis安装后,在src和/usr/local/bin下有几个以redis开头的可执行文件,称为redis shell,这些可执行文件可做很多事情。

image

首先要启动redis服务器

$ redis-server /usr/local/redis/redis.conf

默认是以命令行模式运行,最好如下修改配置以daemon守护进程模式运行:

// 如 /usr/local/redis安装目录,找到redis.conf
daemonize yes

可以使用两种方式连接redis服务器。

第一种:交互式方式

redis-cli -h {host} -p {port}方式连接,然后所有的操作都是在交互的方式实现,不需要再执行redis-cli了。

$redis-cli -h 127.0.0.1-p 6379

127.0.0.1:6379>set hello world

OK

127.0.0.1:6379>get hello

"world"

第二种方式:命令方式

redis-cli -h {host} -p {port} {command}直接得到命令的返回结果。

$redis-cli -h 127.0.0.1-p 6379 get hello

"world"

redis-cli包含很多参数,如-h,-p,要了解全部参数,可用redis-cli -help命令。

第二部分
redis-cli 命令有很多。比如

连接操作相关的命令

默认直接连接 远程连接-h 192.168.1.20 -p 6379
ping:测试连接是否存活如果正常会返回pong
echo:打印
select:切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值
quit:关闭连接(connection)
auth:简单密码认证

  1. 终端redis断开连接

quit

  1. 终端redis插入数据:

set key value [EX seconds] [PX milliseconds] [NX|XX]

常用命令

image

区别
1.set纯设置
2.setnx在key存在则不做任何操作,成功返回1,失败返回0
3.setex设置有效期(必须设置),有效期失效,则key会自动清除:
image

mget、mset

# mset 演示
127.0.0.1:6379> mset key1 val1 key2 val2 key3 val3
OK

# mget 演示
127.0.0.1:6379> mget key1 key2
1) "val1"
2) "val2"

应用场景

缓存、分布式锁、计数器(redis.inc(key))、Session存储、限流
例如,短信发送为了避免接口被频繁调用,通常要在指定时间内避免重复发送

const SMSLimit = async phone => {
    const key = `sms:limit:${phone}`;
    const result = await redis.set(key, 1, 'EX', 60, 'NX');

    if (result === null) {
        console.log('60 秒之内无法再次发送验证码');
        return false;
    }

    console.log('可以发送');
    return true;
}

SMSLimit(18800000000);

数据结构之哈希(hash)

哈希结构有一个特点,所有的命令都是以 H 开头,hash 类型其值本身是由一个或多个 filed-value 构成,如下所示:

hashKey = {
    field1: value1,
    filed2: value2
}
  • 优点:节省空间,可以部分更新。
  • 缺点:不支持 TTL 设置,Redis 中过期时间只针对顶级 Key,无法对 Hash Key 中的 field 设置过期时间,只能对整个 Key 通过 expire 设置。
  • 注意:在使用 hgetall 的时候要注意,如果集合很大,将会浪费性能。

常用命令
image

常用命令实践

# 设置 student 的 name 为 Jack
$ hset student name Jack

# 获取 student 的 name 值
$ hget student name
"Jack"

# 对 key student 批量设置 age、sex 属性
$ hmset student age 18 sex man

# 批量查询 student 的 age、sex 属性
$ hmget student age sex
1) "18"
2) "man"

# 获取 student 的所有 fields、value
$ hgetall student
1) "name"
2) "Jack"
3) "age"
4) "18"
5) "sex"
6) "man"

哈希(hash)应用场景

hash 适合将一些相关的数据存储在一起,例如,缓存用户信息,与字符串不同的是,hash 可以对用户信息结构中的每个字段单独存储,当我们需要获取信息时可以仅获取我们需要的部分字段,如果使用字符串存储,两种方式,一种是将用户信息拆分为多个键(每个属性一个键)来存储,这样就显得有点冗余,占用过的 Key 同时也占用空间,另一种是序列化字符串存储,这种方式如果取数据只能全部取出并且还要进行反序列化,序列化/反序列化也有一定的内存开销。

以下为缓存用户信息代码示例:

// 模拟查询 Mongo 数据
const mongo = {
    getUserInfoByUserId: userId => {
        return {
            name: 'Jack',
            age: 19,
        }
    }
}

// 获取用户信息
async function getUserInfo(userId) {
    const key = `user:${userId}`;
    try {
        // 从缓存中获取数据
        const userInfoCache = await redis.hgetall(key);

        // 如果 userInfoCache 为空,返回值为 {}
        if (Object.keys(userInfoCache).length === 0) {
            const userInfo = mongo.getUserInfoByUserId(userId);

            await redis.hmset(key, userInfo);
            await redis.expire(key, 120);
            return userInfo;
        }

        return userInfoCache;
    } catch(err) {
        console.error(err);
        throw err;
    }
}

getUserInfo(1)

数据结构之列表(list)

Redis 的列表是用来存储字符串元素的集合,基于 Linked Lists 实现,这意味着插入、删除操作非常快,时间复杂度为 O(1),索引很慢,时间复杂度为 O(n)。

Redis 列表的命令都是以 L 开头,在实际应用中它可以用作队列或栈

  • Stack(栈):后进先出,实现命令lpush + lpop
    -Queue(队列):先进先出,实现命令lpush + rpop
    -Capped Collection(有限集合):lpush + ltrim
    -Message Queue(消息队列):lpush + brpop

常用命令
image

常用命令实践

# 列表左侧加入三个元素
$ lpush languages JavaScript Python Go

# 获取列表长度
$ llen languages

# 获取指定范围内的元素列表 lrange key start end(包含end)
# 如果从左到右 start、end 分别为 0、N-1
# 如果从右到左 start、end 分别为 -1、-N
$ lrange languages 0 2
1) "Go"
2) "Python"
3) "JavaScript"

# 列表右端插入元素
$ rpush languages TypeScript

# 再次查看列表的元素
$ lrange languages 0 3
1) "Go"
2) "Python"
3) "JavaScript"
4) "TypeScript"

# 列表左端移除一个元素
$ lpop languages
"Go"

# 列表右侧移除一个元素
$ rpop languages
"TypeScript"

# 设定列表指定索引值为新值
$ lset languages 1 JS

# 列表指定的值前/后插入新值
$ linsert languages after JS Nodejs
(integer) 3

# 按照索引范围修剪列表(元素截取)ltrim key start end
$ ltrim languages 1 2

# lpop/rpop 的阻塞版本,设置 timeout 如果列表为空,客户端将会等待设定的 timeout 时间退出
$ blpop languages 2
(nil)
(2.02s)

应用场景

  1. 消息队列 Redis List 结构的 lpush + brpop 命令可实现消息队列,lpush 命令是从左端插入数据,brpop 命令是从右端阻塞式的读取数据,阻塞读过程中如果队列中没有数据,会立即进入休眠直到数据到来或超过设置的 timeout 时间,则会立即醒过来。
sync function test() {
    const key = 'languages';

    // 阻塞读,timeout 为 5 秒钟
    const result = await redis.brpop(key, 5);

    console.log(result);
}

test();
test();

数据结构之集合(set)

Redis 的集合类型可用来存储多个字符串元素,和列表不同,集合元素不允许重复,集合中的元素是无须的,也不能通过索引下标获取元素。

Redis 集合的命令都是以 S 开头

常用命令
image

常用命令实践

# 集合中添加元素
$ sadd languages Nodejs JavaScript

# 计算集合中元素个数
$ scard languages
(integer) 2

# 判断集合中是否存在指定元素
$ sismember languages Nodejs

# 随机从集合中返回指定元素
$ srandmember languages 2
1) "Nodejs"
2) "JavaScript"

集合间操作

# 设置用户 1 使用的语言
$ sadd user:1 Nodejs JavaScript
(integer) 2
# 设置用户 2 使用的语言
$ sadd user:2 Nodejs Python
(integer) 2

# 求 user:1 与 user:2 交集
$ sinter user:1 user:2
1) "Nodejs"

# 求 user:1 与 user:2 并集
$ sunion user:1 user:2
1) "Python"
2) "Nodejs"
3) "JavaScript"

# 求 user:1 与 user:2 差集
$ sdiff user:1 user:2
1) "JavaScript"

应用场景

  1. 抽奖

Redis 的集合由于有去重功能,在一些抽奖类项目中可以存储中奖的用户 ID,能够保证同一个用户 ID 不会中奖两次。

async function test(userId) {
    const key = `luck:users`;
    const result = await redis.sadd(key, userId);

    // 如果元素存在,返回 0 表示未添加成功
    if (result === 0) {
        console.log('您已中奖 1 次,无法再次参与');
        return false;
    }

    console.log('恭喜您中奖');
    return true;
}

test(1);
  1. 计算用户共同感兴趣的商品
    sadd + sinter 可用来统计用户共同感兴趣的商品,sadd 保存每个用户喜欢的商品标签,使用 sinter 对每个用户感兴趣的商品标签求交集。

数据结构之有序集合(zset)

Redis 的有序集合(zset)保留了集合(set)元素不能重复的特性之外,在有序集合的元素中是可以排序的,与列表使用索引下标不同的是有序集合是有序集合给每个元素设置一个分值(score)做为排序的依据。

Redis 有序集合的命令都是以 Z 开头
image

常用命令实践
sadd

Redis 3.2 对 zadd 增加了三个选项 [NX|XX]、[CH]、[INCR]:

  • [NX|XX]:NX,member 必须不存在才添加成功,用于 Create;XX,member 必须存在才可更新成功,用于 UPDATE。
  • [CH]:返回此次操作后有序集合元素和分数发生的变化
  • [INCR]:对 score 做增加,相当于 zincrby
  • score:代表分数(排序)
  • member:成员
zadd key [NX|XX] [CH] [INCR] score member [score member ...]

集合的增删改查

# 有序集合 grades 中添加 3 个元素
$ zadd grades NX 80 xiaoming 75 xiaozhang 85 xiaoli
(integer) 3

# 查看成员 xiaozhang 分数
$ zscore grades xiaozhang
"75"

# 更新成员 xiaozhang 分数
$ zadd grades XX 90 xiaozhang

# 再次查看成员 xiaozhang 分数
$ zscore grades xiaozhang
"90"

# 查看成员排名
$ zrank grades xiaozhang # 分数从低到高返回排名
(integer) 2
$ zrevrank grades xiaozhang # 分数从高到底返回排名
(integer) 0

# 增加成员分数
$ zincrby grades 5 xiaozhang
"95"

# 返回指定范围成员排名,WITHSCORES 可选参数,去掉则不反回分数
$ zrange grades 0 2 WITHSCORES
1) "xiaoming"
2) "80"
3) "xiaoli"
4) "85"
5) "xiaozhang"
6) "95"

# 返回指定分数范围内的成员列表
$ zrangebyscore grades 85 100
1) "xiaoli"
2) "xiaozhang"

# 删除指定成员
$ zrem grades xiaoli
(integer) 1

应用场景
Redis 的有序集合一个比较典型的应用场景就是排行榜,例如,游戏排行榜、用户抽奖活动排行榜、学生成绩排行榜等。

Redis 主从复制

单机带来的问题机器故障、容量限制、QPS瓶颈,主从复制是一种 一主多从 的模式提供了数据副本,解决了单机带来的机器故障问题,另外主从分离模式还提高了 Redis 读的性能。也是高可用、分布式的基础。

存在的问题

主节点故障

假设主节点发生故障之后,这个时间的数据将会丢失,因为从节点仅是主节点的一个备份节点,这个时候就需要故障转移,手动的去选一个 slave 做为主节点去工作,显然手动这样不是很好的。

读写能力首受限

只能在一个节点存储,其它的 slave 节点只是主节点的一个副本。

解决方案

针对这些问题我们在下节 Sentinel 中会讲解。

Sentinel

上面提到的 Redis 主从复制中,如果主节点发生故障,我们是希望从节点可以自动提升而不是人工来干预,Redis 提供的 Sentinel 功能就可实现此功能。

Sentinel简介

edis Sentinel 是一个分布式系统,类似于 Consul 集群,一般由 3 ~ 5 个节点组成,使用 Raft 算法实现领导者选举因为故障转移工作只需要一个 Sentinel 节点来完成,如下图所示,我们客户端部分直接和 Sentinel 集群交互,关于 Redis 主从节点的状态维护交由 Sentinel 去管理。
image

Nodejs客户端链接

客户端通过 Sentinel 发现主从节点地址,然后在通过这些地址建立相应的链接来进行数据存取操作(也就是自动去找master)

const Redis = require('ioredis');
const redis = new Redis({
    sentinels:[
        { host: '192.168.6.128', port: 26379 },
        { host: '192.168.6.129', port: 26379 },
        { host: '192.168.6.130', port: 26379 },
    ],
    name: 'mymaster',
});

let count = 0;
setInterval(async function() {
    count++;
    const key = `k_${count}`;

    try {
        await redis.set(key, count);
        console.log(key, redis.get(key));
    } catch (err) {
        console.error(err);
    }
}, 1000)

Node.js 中实践 Redis 分布式锁

在一些分布式环境下、多线程并发编程中,如果对同一资源进行读写操作,避免不了的一个就是资源竞争问题,通过引入分布式锁这一概念,可以解决数据一致性问题。

认识线程、进程、分布式锁

线程锁:单线程编程模式下请求是顺序的,一个好处是不需要考虑线程安全、资源竞争问题,因此当你进行 Node.js 编程时,也不会去考虑线程安全问题。那么多线程编程模式下,例如 Java 你可能很熟悉一个词 synchronized,通常也是 Java 中解决并发编程最简单的一种方式,synchronized 可以保证在同一时刻仅有一个线程去执行某个方法或某块代码。

进程锁:一个服务部署于一台服务器,同时开启多个进程,Node.js 编程中为了利用操作系统资源,根据 CPU 的核心数可以开启多进程模式,这个时候如果对一个共享资源操作还是会遇到资源竞争问题,另外每一个进程都是相互独立的,拥有自己独立的内存空间。关于进程锁通过 Java 中的 synchronized 也很难去解决,synchronized 仅局限于在同一个 JVM 中有效。

分布式锁:一个服务无论是单线程还是多进程模式,当多机部署、处于分布式环境下对同一共享资源进行操作还是会面临同样的问题。此时就要去引入一个概念分布式锁。如下图所示,由于先读数据在通过业务逻辑修改之后进行 SET 操作,这并不是一个原子操作,当多个客户端对同一资源进行先读后写操作就会引发并发问题,这时就要引入分布式锁去解决,通常也是一个很广泛的解决方案。

image

基于 Redis 的分布式锁实现思路

实现分布式锁的方式有很多:数据库、Redis、Zookeeper。这里主要介绍的是通过 Redis 来实现一个分布式锁,至少要保证三个特性:安全性、死锁、容错。

安全性:所谓一个萝卜一个坑,第一点要做的是上锁,在任意时刻要保证仅有一个客户端持有该锁。

死锁:造成死锁可能是由于某种原因,本该释放的锁没有被释放,因此在上锁的时候可以同步的设置过期时间,如果由于客户端自己的原因没有被释放,也要保证锁能够自动释放。

容错:容错是在多节点的模式下需要考虑的,只要能保证 N/2+1 节点可用,客户端就可以成功获取、释放锁。

Redis 单实例分布式锁实现

上锁

上锁的第一步就是先通过 setnx 命令占坑,为了防止死锁,通常在占坑之后还会设置一个过期时间 expire,如下所示:

setnx key value
expire key seconds

以上命令不是一个原子性操作,所谓原子性操作是指命令在执行过程中并不会被其它的线程或者请求打断,以上如果 setnx 执行成功之后,出现网络闪断 expire 命令便不会得到执行,会导致死锁出现。
现在 Redis 官方 2.8 版本之后支持 set 命令传入 setnx、expire 扩展参数,这样就可以一条命令一口气执行,避免了上面的问题,如下所示:

set key value [EX seconds] [PX milliseconds] [NX|XX]

释放锁

释放锁的过程就是将原本占有的坑给删除掉,但是也并不能仅仅使用 del key 删除掉就万事大吉了,这样很容易删除掉别人的锁,为什么呢?举一个例子客户端 A 获取到一把 key = name1 的锁(2 秒中),紧接着处理自己的业务逻辑,但是在业务逻辑处理这块阻塞了耗时超过了锁的时间,锁是会自动被释放的,这期间该资源又被客户端 B 获取了 key = name1 的锁,那么客户端 A 在自己的业务处理结束之后直接使用 del key 命令删除会把客户端 B 的锁给释放掉了,所以释放锁的时候要做到仅释放自己占有的锁。

加锁的过程中建议把 value 设置为一个随机值,主要是为了更安全的释放锁,在 del key 之前先判断这个 key 存在且 value 等于自己指定的值才执行删除操作。判断和删除不是一个原子性的操作,此处仍需借助 Lua 脚本实现。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

企业对外的服务redis注意

image

常见问题

(error) MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.

解决:

127.0.0.1:6379> config set stop-writes-on-bgsave-error no

问题

1.ERR max number of clients reached
使用redis-cli进入命令行,使用info clients 查看客户端连接数,然后使用client list查看当前连接的客户端。
设置参数timeout(客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能)自动断开连接,键入config set timeout 600

参考

redis五种数据结构
Redis,就是这么朴实无华