questions
使用go-redis连接,因为redigo无法连接redis集群
redis集群可以用kratos的或者go-zero的分布式锁实现
https://juejin.cn/post/7041375517580689439
go-redis 连接集群
1 | func initClient()(err error){ |
go-redis 连接哨兵
1 | func initClient()(err error){ |
Redis数据类型
String | List | Hash | Set | Zset |
---|---|---|---|---|
SDS | QuickList | Dict、ZipList | Dict、Intset | SkipList |
# redis内存模型? | ||||
hashtable |
特殊数据类型
- BitMap:签到、行为统计(点赞)
- hyperloglog:不太了解
- Geospatial:基于sort set,GEO 中存储的地理位置信息的经纬度数据通过 GeoHash 算法转换成了一个整数,这个整数作为 Sorted Set 的 score(权重参数)使用。
【重点】一致性问题:Redis和数据库的一致性
https://juejin.cn/post/7287026079066800168#heading-1
【重点】redis主从的一致性【配置redis.conf】
- 全量复制,master->slave1->slave2的级联方式
- 全量
- salve 发送sync请求到master,开始第一次同步
- 第一次同步时使用bgsave做rdb快照,同时将后续修改记录加到内存缓冲区,完成后将rdb文件同步到从节点,复制完后由从节点加载到内存
- 加载完成后通知master,master将缓冲区的写操作记录发给slave,slave再执行剩余的这些写操作,与master保持一致
- 部分复制
- slave发送psync请求到master,开始第一次同步
- slave再发一个偏移量,master从这个偏移量开始同步数据
三种缓存读写策略
1. Cache Aside Pattern(旁路缓存模式)
- 写:先更新DB,再删除cache,先dao.create/update/delete,再更新缓存,用redis实例去set更新key
- 读:先从cache读,读到就返回;读不到就读db,将数据写到cache ,先从redis.get,如果非空,用dao查找
- 为什么要删缓存而不是更新?删除轻量一些,更新比较耗时,数据重新回缓存由更多的读操作实现
- 【问题】可以先删cache,再更新db吗?
不能,因为写的时间远大于读,出现数据不一致的可能性更高,因为在更新db前,cache可能已经被读操作覆盖了缺陷有哪些
- 不能避免个别的不一致性问题,就是脏读问题
- 写操作多的情况影响性能,因为每次都要更新db
- 在高并发的情况下,不管是先写数据库,再删缓存;还是先删缓存,再写数据库,都有可能出现数据不一致的情况,比如:
1
2
3
4
5
6
7
8
9如果删除了缓存redis,还没来得及写库mysql,另一个线程就读取,发现缓存为空,则去数据库读取数据写入缓存,此时缓存中的数据为脏数据。
如果写了库,在删除缓存前,写库的线程故障了,也会出现数据不一致的情况。
解决办法:
延迟双删策略
1、先删除缓存
2、再写数据库
3、休眠时间(根据统计线程读取数据和写缓存的时间)
(休眠的作用是当前线程等其他线程读完了数据后写入缓存后,删除缓存)
4、再删除缓存
解决方法
解决办法:
- 数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
- 可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小
- 还有一个阿里的canal组件,是监控mysql的binlog,更改了就自动去写redis
2. Read/Write Through Pattern(读写穿透)
与读写穿透的区别::Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
Redis持久化
Redis持久化的方式
Redis 共有三种数据持久化的方式:
- AOF(Append Only File)日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
- RDB(Redis Database Backup file) 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
- 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点
RDB快照
提供两个命令实现快照 - save:在主线程实现,可能会导致阻塞
- bgsave:background save,在后台的子进程生成RDB快照
RDB 在执行快照的时候,数据能修改吗?
执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于【写时复制技术】(Copy-On-Write, COW)。
技术原理:bgsave会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。
AOF日志
在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
它们的区别是什么
AOF:三种写回方式
所以,RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
AOF 日志过大,会触发什么机制
【AOF 重写机制】,压缩AOF文件:
【压缩方式】:在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。但是对KV的记录就保持最新的那一条
redis事务【不建议开发时使用,和mysql不一致的时候会造成缓存读写的限制问题】
不支持回滚
- ULTI/EXEC 命令:
在 Redis 中,事务的开始由 MULTI 命令表示,结束由 EXEC 命令表示。在 MULTI 和 EXEC 之间的所有命令会被添加到事务队列中,但不会立即执行。 - WATCH 命令:
Redis 提供了 WATCH 命令,可以用于在事务执行之前监视一个或多个键。如果在事务执行过程中,被监视的键被其他客户端修改了,事务将会被打断。
数据类型实现
String(字符串) 应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
- 底层数据结构是SDS(Simple Dynamic String)简单动态字符串,保存文本数据,还可以保存二进制数据 。因为 SDS 使用 len 属性 的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
- Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出 :因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
使用 String 来缓存对象有两种方式:
直接缓存整个对象的 JSON,命令例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值,命令例子: MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20**
### List(列表)
- **数据结构**:quicklist(双向链表+压缩列表)
- **类型的应用场景**:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一ID;2. 不能以消费组形式消费数据)等
**List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列。**
- 生产者使用 LPUSH key value[value...] 将消息插入到队列的头部,如果 key 不存在则会创建一个空的队列再插入消息。
- 消费者使用 RPOP key 依次读取队列的消息,先进先出。
### Set(集合) 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- **数据结构**:是由哈希表或整数集合实现的
- 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
- 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
### Zset(有序集合) 类型:排序场景,比如排行榜、电话和姓名排序等
- **数据结构** :使用 跳表 实现的
-
#### redis队列和延时队列
- 队列使用list,当队列为空,rpush生产消息,使用blpop消费消息。
- 延时队列使用zset,每个消息对应的时间戳作为score,消息内容当key,**zadd生产消息**,消费者用 **zrangebyscore 指令获取 N 秒之前的数据轮询进行处理**
### Hash(哈希) 类型:缓存对象、购物车等。
### BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、 连续签到用户总数等;
### HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
### GEO
### Stream
# Redis 的线程模型
**首先**,是单线程模型,它指的是```「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」```这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)
### Redis 6.0 之后为什么引入了多线程?
回答:网络IO出现瓶颈,对网络IO引入了多线程处理,命令执行仍然是主线程完成。
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为```随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上```。
所以为了提高网络 I/O 的并行度,Redis 6.0 **对于网络 I/O 采用多线程来处理**。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。
### Redis的零拷贝技术是什么
能省下拷贝开销的地方,一般直接传文件描述符的地址去操作,Linux的sendfile,还有实现内存映射
# Redis集群
主从复制,主节点故障时要手动恢复
### 主从模式
读写分离,主节点负责写,从节点负责读。
### 哨兵模式
多个哨兵监控主节点服务器,提供故障转移功能:
【故障转移】:主节点挂了之后,在从节点中选取一个作为主节点
### 切片集群模式
缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群。
将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
- 切片就是一个redis实例分成多个hash slot,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
- 默认部署的slot个数有16384个,平均分配到各节点上,如果有n个redis实例,那么每个节点有16384/n个slot。
### 可能出现的问题:
**集群脑裂是什么**:
由于网络问题,导致主节点与哨兵失联后,哨兵多选举出来一个主节点,当旧节点恢复正常时,降级从节点后,向新master请求同步复制时,清空了自己的缓冲区,产生了之前客户端写入的数据丢失的问题。
- 如果旧节点又好了,就把旧主节点降级为普通节点,作为从节点向新master进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
【解决方案】
当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
【配置文件】
- min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
- min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
### Redis过期删除与内存淘汰
【过期删除:惰性删除+定期删除】当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
- 惰性删除:惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
- 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
【内存淘汰:不进行数据淘汰的策略/进行数据淘汰的策略】
### 集群与哨兵模式的区别
- 主从集群模式适合对读写性能要求高,且可以容忍一定程度的数据同步延迟的场景
- 哨兵模式适用于对高可用性要求较高的场景,能够实现自动故障切换
## Redis的lua支持
如果你想在 Redis 中定时执行 Lua 脚本,可以考虑使用 Redis 的定时任务功能,例如使用 Redis 的BGSAVE和MONITOR命令配合实现。
1. 编写 Lua 脚本
首先,你需要编写一个 Lua 脚本,命名为 a.lua 或其他你喜欢的名字。在该脚本中编写你想要定时执行的逻辑。
2. 使用 BGSAVE
Redis 的 BGSAVE 命令用于在后台执行持久化操作(将数据写入磁盘),这会创建一个快照文件。你可以利用这个特性来触发 Lua 脚本的执行。
客户端执行```BGSAVE
请注意,BGSAVE 不会阻塞 Redis 的主线程,因此可以在 Redis 运行时执行。
- 使用 MONITOR
Redis 的 MONITOR 命令可以用于实时监控 Redis 的命令执行情况。你可以通过监控 Redis 的命令来捕捉 BGSAVE 命令的执行,一旦发现 BGSAVE 命令执行完毕,就可以在 Lua 脚本中调用 EVAL 来执行你的逻辑。
Redis缓存
介绍缓存雪崩,缓存击穿,缓存穿透
- 缓存雪崩 指大量缓存数据在同一时间过期时,大量的用户请求全部直接访问数据库,从而导致数据库崩溃的问题,从而形成一系列连锁反应,造成整个系统崩溃。
- 缓存击穿 指某个数据过期时,大量用户请求直接访问该数据,导致高并发的数据库请求
- 缓存穿透 当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
【缓存雪崩解决方法】
- 设置缓存失效时间随机打乱
- 设置多级缓存
- 设置缓存不过期,使用后台接口进行操作redis,不推荐,人工要考虑的太麻烦
【缓存击穿解决方法】
- 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
【缓存穿透解决方法】
布隆过滤器:快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
设置空值或者默认值
在API入口处判断请求参数有没有非法值/是否存在
热点数据缓存策略
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
【面试题】缓存更新策略
- Cache Aside(旁路缓存)策略;
- Read/Write Through(读穿 / 写穿)策略;
- Write Back(写回)策略;
【面试题】数据库和缓存如何保证一致性
【面试题】常见性能问题和解决方案
- Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave 函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大 的,会间断性暂停服务
- 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局 域网
- 尽量避免在压力很大的主库上增加从
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1<- Slave2 <- Slave3… 这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂了,可以立刻启 用 Slave1 做 Master,其他不变。
过期key的删除策略
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选 最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选 将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任 意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘 汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据 注意这里的 6 种机制,volatile 和 allkeys 规定了是对已设置 过期时间的数 据集淘汰数据还是从全部数据集淘汰数据,后面的 lru、ttl 以及 random 是 三种不同 的淘汰策略,再加上一种no-enviction 永不回收的策略。
如何选取上述策略?
- 如果数据分布的差不多,使用allkeys random
- 如果数据分布差别大,使用allkeys lru
redis集群的原理
哨兵模式,高可用性,在master宕机时自动将slave提升为master
集群模式,扩展性,单个redis内存不足时,使用cluster进行分片存储
当发生故障转移(failover)时,在连接丢失的情况下,部分写操作无法完成
另外如果用了RDB,主节点写操作存在buffer里,转移主节点时,这部分不进行复制,导致写操作丢失,所以可以使用混合方式,写操作使用AOF持久化一下,转移主节点后重放AOF日志
redis事务了解吗
multi exec discard watch
如何优化redis内存占用和性能【内存优化】
对小数据合并到一个对象中,用hash存储
设置合理的过期策略,和内存淘汰策略等
使用持久化保证高可用性
使用布隆过滤器,防止缓存穿透和击穿问题,查看一个元素是否存在于一个集合中
删除key后的碎片整理:Redis 会在删除键值对后,释放内存并且尝试整理内存碎片。可以通过配置文件中的 activerehashing 参数来控制内存碎片整理的行为。
假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某 个固定的已知的前缀开头的,如果将它们全部找出来?
- 使用scan无阻塞地去提取正则字符串,然后在客户端用一个set去重
- keys也可以扫,但是会导致阻塞,线上服务会停
redis内存回收进程
https://cloud.tencent.com/developer/article/2315748
Redis回收进程指对那些已过期但是尚未被删除的 keys 进行标记,这样它们就可以在之后被立即释放并回收所占用的内存
- 基本原理是周期性地扫描存储数据库中所有的键
1
2
3
4
5
6
7
8
9Redis 提供了三个与内存回收相关的命令:
MEMORY USAGE key:
用于返回指定键所占用的内存字节数。可以通过传递键的名称作为参数来获取相应键的内存使用情况。
MEMORY PURGE:
该命令用于在 Redis Enterprise 中手动触发内存回收。
MEMORY DOCTOR:
该命令用于诊断 Redis 内存分配和使用情况,帮助识别内存泄漏或者不正常的内存使用情况。
需要注意的是,MEMORY PURGE 和 MEMORY DOCTOR 是 Redis Enterprise 特有的命令,而 MEMORY USAGE 是 Redis 通用的命令。
如何使用redis实现一个分布式锁
- set if not exist拿锁,拿到之后expire给锁加一个过期时间,
Redis内存耗尽会怎样
崩溃,可能导致缓存失效,命中率下降,虚拟内存
https://juejin.cn/post/6932711444404256781
- 会使用LRU和LFU的内存淘汰策略
LRU 最近最长时间未被使用
LFU 最近最少频率使用
stream数据结构
- 基于基数树
缓存穿透miss 和击穿breakdown 怎么解决
- 在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回
- 简单处理,存无效key,value设为null
如果有大量的key需要同一时间过期,要如何解决缓存雪崩问题
- 设置过期时间时加上随机值,使得缓存失效的时间点尽量均匀分布。
- 使用 Redis 集群,将缓存数据分散到多个节点上,避免单点故障。
- 在缓存失效后采用加锁或者队列来控制读数据库写缓存的线程数量,避免大量线程同时读数据库。
- 针对热点数据可以设置永不过期,或者使用手动过期的方式来控制缓存的使用时间
- 高并发时,使用限流和熔断机制控制请求访问量
- 本地和分布式缓存结合,服务器本地当二级缓存
热key问题怎么解决
热key问题是由于某部分热点key分布在不同的节点上,导致负载不均衡
- 解决方法,1使用分布式缓存,读写分离架构 2 数据分片策略 3 缓存失效策略避免一直是热key
- 如果热Key的产生来自于读请求使用读写分离架构
您可以将实例改造成读写分离架构来降低每个数据分片的读请求压力,甚至可以不断地增加从节点。但是读写分离架构在增加业务代码复杂度的同时,也会增加Redis集群架构复杂度。不仅要为多个从节点提供转发层(如Proxy,LVS等)来实现负载均衡,还要考虑从节点数量显著增加后带来故障率增加的问题。单例的redis能承载多少个连接?【默认1w个,可以用maxclients修改】
redis 6.0 前后不支持与支持多线程的理由
- 6.0前,避免竞态条件,上下文切换的开销,充分利用CPU
- 6.0后的多线程主要引入的是IO和AOF和RDB备份
提高命中率的方式
- 缓存过期时间
- 缓存预热
- LRU LFU分别基于访问时间和频率来确定缓存中的数据
- 使用分布式缓存,将缓存数据分布到多节点上
redis 如何解决key冲突
命名时注意不冲突,比如加前缀后缀
不同数据存合适的数据结构
分布式锁来保证并发冲突
单线程下使用mutex方法
内存模型是hashtable,解决key冲突可能就链地址 开地址哪些吧
redis 如何解决大key问题【key的value过大】
大key产生的问题
- 执行变慢,删除时产生阻塞,内存溢出
处理方式
- 如果已经发现了一个大key,就遍历把它插成大key1 key2 key3,限制长度
- 不要这个大key时,使用UNLINK删除
- 使用redis分片技术:
一致性hash:
将哈希值映射到一个固定大小的环形空间中。客户端根据键的哈希值定位到环上的某个位置,然后找到离该位置最近的节点,将数据存储在该节点上。- 优点:在节点的增减时,只有少量的数据需要重新映射,保持了相对的稳定性。
- 缺点:可能会出现不均匀的数据分布,导致节点负载不均
CRC16:
循环冗余校验来生成哈希值- 优点:计算速度快,适用于一些简单的分布式场景。
- 缺点:可能会导致节点负载不均衡。
RedisCluster
它将数据分片到多个节点上,同时提供了节点间的数据复制和故障恢复机制- 优点自动进行数据分片和复制,实现了高可用性。
- 缺点好用但是复杂
1
2
3Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。
redis慢查询如何排查
命令:
- 慢查询日志:SHOWLOG GET
- redis-cli的INFO
- redis有一个时延监控命令,–latency查询命令