redis权威指南
2024-02-21 11:37:56 1 举报
AI智能生成
redis指南
作者其他创作
大纲/内容
过期/淘汰策略
过期策略
策略方式
定时过期策略
到了时间就立即移除(对内存友好,但会占用大量cpu)
惰性过期策略
当访问一个key的时候再判断是否过期(对内存不够友好)
定期过期策略
每过一段扫描一次数据,清楚过过期的
redis采用惰性过期策略和定期过期策略
redis通过expire和persist(移除可以的过期时间)设置过期时间和永久生效
淘汰策略
指缓存的内容不足,怎么处理需要新写入需要额外空间的数据
策略方式
全局的键空间选择性移除
allkeys-lru
最近最少使用
allkeys-random
任意淘汰一个
no-enviction
禁止淘汰,新写入会报错,但保证数据不会丢失,系统默认的方式
设置过期时间的键空间移除
volatile-lru
最近最少使用的数据
volatile-ttl
最接近淘汰时间的数据
volatile-random
随机移除数据
内存相关
内存回收默认使用LRU算法,可以配置淘汰策略
优化
充分利用各个数据类型
通过 maxmemory 参数来限制最大可用内存
线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
参考:https://www.cnblogs.com/winner-0715/p/8733787.html
事务
概述
通过multi\exec\watch执行的一组命令集合;事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照
顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
将一个事务的所有命令序列化,然后按顺序执行
要点
- 不支持回滚;
- 一个事务中有命令错误则所有的都不执行;
- 出现运行错误,正确的命令会被执行
Redis 2.6.5之前的版本会忽略有语法错误的命令
三阶段
- 开启事务multi
- 命令入队列
- 事务执行exec
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把其他请求放入队列中排队
相关命令
multi
开启一个事务
exec
执行事务
discard
客户端可以放弃一个事务队列,并放弃执行事务,客户端会从事务状态退出
watch
命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
unwatch
只支持一致性和隔离性,在AOF先开启aways备份时具有持久性,不具备原子性
其他实现方式
基于LUA代码
基于中间标记量
集群
哈希槽
Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
redis集群不保证数据的强一致性,在实际中集群在特定的条件下可能会丢失写操作
主从架构
主要用来支撑读高并发,一主多从
复制机制
一个 master node 是可以配置多个 slave node 的
slave node 也可以连接其他的 slave node
slave node 做复制的时候,不会 block master node 的正常工作
slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;
但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
要点
建议必须开启 master node 的持久化
如果slave持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。
核心原理
1. 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
3. 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
4. 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
5. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致
缺点
所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决
集群方案
哨兵模式(sentinel)
(主从复制的一种实现)
(主从复制的一种实现)
主要功能
监控
监控master和slaver的进程是否正常
消息通知
如果某个节点处故障,发消息通知管理员
故障转移
如果master挂了,自动转移到slave节点
高可用实现方法
本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
要点
至少需要3个实例保障自身的健壮性
哨兵集群+redis主从复制部署不能保证数据零丢失,只能保证高可用
(master数据还未同步至slaver,master服务挂掉,此时重新选举master,不会有之前master为同步到slaver的数据)
(master数据还未同步至slaver,master服务挂掉,此时重新选举master,不会有之前master为同步到slaver的数据)
概述
哨兵节点
特殊的redis节点,不存数据
监控
sentinel会不断检测主节点和从节点是否正常
提醒
当检测某个节点时,可以通过API像管理员和其他应用发送通知
自动故障转移
当一个主服务不能正常工作时会开始一次故障处理操作
数据节点
主节点和从节点都是数据节点
官方集群方式-分布式集群
(服务器路由)
(服务器路由)
没有采用一致性哈希算法,采用槽(slot)的概念,一共分为16384个槽;
请求发送到任一节点,都会被正确路由(在客户端的帮助下直接redirected到正确的redis节点)到正确的节点
请求发送到任一节点,都会被正确路由(在客户端的帮助下直接redirected到正确的redis节点)到正确的节点
方案说明
1. 通过哈希的方式,将数据分片,每个节点(主多从)均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
2. 每份数据分片会存储在多个互为主从的多节点上
3. 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
4. 同一分片多个节点间的数据不保持一致性
5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
6. 扩容时需要把旧节点的数据迁移一部分到新节点
要点
每个redis要开放两个端口,一个对外提供使用,一个用于节点间通讯
节点间通讯采用cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一
种二进制的协议, gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间
种二进制的协议, gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间
gossip协议
节点间内部通讯机制
集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。
优点
无中心架构支持动态扩容
具有哨兵模式的监控和故障转移能力
客户端不需要连接所有节点,连接任一节点即可
高性能,直接连接服务,免去了中间代理层
缺点
运维复杂,数据迁移需要人工干预
只能使用0号数据库
不支持批量操作
分布式逻辑和存储模块耦合
基于客户端分配
采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上
优点
服务端彼此独立无相互关联,每个redis像独立的服务一样
非常容易线性扩展,系统灵活性强
缺点
sharding处理放到客户端,规模进一步扩大时给运维带来挑战
不支持动态增删节点。
服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。
连接不能共享,当应用规模增大时,资源浪费制约优化
服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。
连接不能共享,当应用规模增大时,资源浪费制约优化
基于代理服务器分片
代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端
特征
透明接入,业务程序不用关心后端Redis实例,切换成本低
Proxy 的逻辑和存储的逻辑是隔离的
代理层多了一次转发,性能有所损耗
业界开源方案
Twtter开源的Twemproxy
豌豆荚开源的Codis
概述
优点
读写性能优异-读11万,写8万
支持持久化数据
RDB
AOF
支持事务
所有命令操作都是原子性
支持多个命令合并打包的原子执行(打包顺序执行,之间不执行别的命令,别的命令进入等待队列)
数据结构丰富
支持主从复制
主机自动将数同步给从机
缺点
受物理内存限制,不能作为海量数据高速读写
不具备自动容错和恢复功能,主从机的宕机都会导致部分请求的失败
主机宕机前,部分数据未同步从机,会引起数据不一致和数据丢失的问题
在线扩容较难
集群容量达到上线时再次扩容难度较大
高性能
将用户访问的数据存在缓存中
高并发
将数据库中部分数据移到缓存中
redis与Map/guava的优势
map/guava
在多实例下,各个实例单独拥有缓存,导致缓存不一致
速度快的原因
完全基于内存
数据结构简单,操作也简单
采用单线程设计,避免上下文切换和竞争条件
新版有引入多线程操作,但仅是对一些删除操作使用多线程:对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率
使用多路I/O复用模型,非阻塞IO
https://www.processon.com/mindmap/5eaa3ab5e401fd21c19cdf2a
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,
Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
应用场景
计数器
缓存(内容可以失效)
数据库缓存、会话缓存、全网页缓存(FPC)
查找表(内容不能失效)
消息队列(订阅/发布功能)
分布式锁
redis支持的setnx实现分布式锁
官方推荐的RedLock分布式锁
数据类型
基本数据类型
字符串、整数、浮点数(String)
对字符串或字符串部分进行操作;
对数据可以进行自增减操作
对数据可以进行自增减操作
列表(List)
从两端压入或弹出元素;对单个或多个进行修剪操作;保留一定范围的元素
集合(Set)
添加、获取、移除单个元素,检查一个元素是否存在,计算交、并、差集计算,在集合里面随机获取元素
散列表(Hash)
添加、获取、移除、单个键值对;获取所有键值对;检查某个键是否存在
有序集合(ZSet)
添加、获取、删除元素;根据分值范围或成员获取元素;计算一个键的排名
特殊数据类型
地理位置(geospatial)
HyperLogLog 是用来做基数统计的算法
位存储(bitmaps)
pub/sub
stream
持久化的pub/sub
redis5.0新增
借鉴kafka的模式
持久化
RDB(redis database缩写快照)
按照一定的时间将内存数据以快照的形式保存到硬盘中
优点
只有一个dump.rdb文件,方便持久化
容灾性好,一个文件可以保存在安全磁盘
性能最大化,子进程完成数据备份,不影响主进程
数据集大时比AOF恢复快
性能比AOF好
缺点
数据安全性低,隔一段时间备份会有数据丢失的问题
AOF(append only file)
将redis执行的每次命令放到一个日志文件中记录;当开启两种方式备份时,优先使用AOF恢复数据
优点
数据安全,可以做到记录备份每一条数据
rewrite模式会对过大的文件进行合并重写,删除其中的某些命令
比RDB方式更安全
缺点
AOF文件比RDB文件大,数据恢复启动时慢
redis持久化数据和缓存扩容的方式
缓存扩容方式
使用一致性哈希实现动态扩容缩容
持久化数据扩容方式
必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情
况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
分区
原因
分区可以让Redis管理更大的内存
没有分区,你最多只能使用一台机器的内存
分区方案
客户端分区
在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取
代理分区
代理根据分区规则决定请求哪些Redis实例
查询路由
客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点
缺点
涉及多个key的操作通常不会被支持
例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用 交集指令)
同时操作多个key,则不能使用Redis事务.
分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集
当使用分区的时候,数据处理会非常复杂
例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件
分区时动态扩容或缩容可能非常复杂
Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,
但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题
但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题
分布式问题
分布式锁
单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁
SETNX命令
设置成功,返回 1 。设置失败,返回 0
流程
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
为了防止获取锁后程序出现异常进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除;
对应的如果处理耗时操作,也会涉及续命操作(在过期时间到达之前,修改过期时间,使得继续使用锁)
对应的如果处理耗时操作,也会涉及续命操作(在过期时间到达之前,修改过期时间,使得继续使用锁)
存在的问题
有效期导致的安全问题
A拿到的锁已经失效,但是还在操作资源,此时B服务拿到锁。则存在并发数据安全问题(AB服务同时访问资源)
解决方案:续命
另外线程异步通知更改加锁时间
优雅实现阻塞加锁
解决方案:mq通知
主从模式下,在master中拿到锁,还未同步至slave,master宕机导致的并发问题
不建议使用redis分布式锁,建议使用zookeeper实现
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
redlok
官方放提出的redis分布式锁方案
集群环境下,依次从各个节点获取锁,半数以上节点获取成功才真正获取锁成功,否则释放已经获取的锁
特征
安全特性
互斥访问,即永远只有一个 client 能拿到锁
避免死锁
最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
容错性
只要大部分 Redis 节点存活就可以正常提供服务
数据结构
SDS动态字符串
多增加len记录字符串长度
自动扩展空间
空间不够的话,SDS 会自动扩展空间,避免了像 C 字符串操作中的覆盖情况;
有效降低内存分配次数
空间预分配
空间惰性释放
二进制安全
C 语言字符串只能保存 ascii 码,对于图片、音频等信息无法保存,SDS 是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;
字典(相当于hash)
扩容
当 hash 表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2 倍。
不过如果 Redis 正在做 bgsave(持久化命令),为了减少内存也得过多分离,Redis 尽量不去扩容,
但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
不过如果 Redis 正在做 bgsave(持久化命令),为了减少内存也得过多分离,Redis 尽量不去扩容,
但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
缩容
元素个数低于数组长度的10%会缩容
缩容操作不会考虑bgsave
缩容操作不会考虑bgsave
跳跃表
压缩表(ziplist)
zset,hash会在元素比较少的时候采用压缩存储
压缩表是一块连续的内存空间,元素之间数据紧挨着没有空隙
应用
列表
当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表的底层实现。
哈希
当一个哈希只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希的底层实现。
快速列表(quicklist)
list+zipList
redis3.2引入用于替代ziplist 和 list
缓存
缓存异常
缓存雪崩
缓冲同一时间大量失效
解决方案
过期时间设置随机值,避免大量失效
并发不大的情况可以使用加锁排队
给每个缓存增加是否失效标记,失效后马上刷新缓存
缓存穿透
指缓存和数据库中都没有的数据
解决方法
接口增加校验拦截无效请求
在数据库中没有取到的数据也可以缓存,设置短时间失效
采用布隆过滤器
将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的
查询压力
查询压力
缓存击穿
缓存中没有,但数据库中有(一般为缓存时间到期)
解决方案
热点数据永不过期
加互斥锁
缓存预热
系统上线时,将相关数据直接缓存到缓存系统
方案
管理界面手动管理
系统启动时直接加载(数据量不大的情况)
定时任务刷新
缓存降级
服务出现异常或非核心业务影响核心业务,仍然保证核心系统能使用,可以根据关键数据判断实现缓存降级或者手动降级
目的
保障核心业务可用
为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略
其他
redis为单线程应用,可以在一个节点部署多个redis提高CPU利用率
一个数据至少读取两次才有缓存意义
缓存与数据库在去写数据时的一致性问题
1.非不必须情况,建议允许有偶尔的不一致情况
2.串行化
读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
缺点
吞吐量大幅降低
3.先更新数据库,再删除缓存(建议使用)
缺点
可能会暂时产生不一致的情况,但是发生的几率特别小
常见性能问题和解决方案
1. Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次(可配置为每执行一个命令就持久化一次)。
3. 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
4. 尽量避免在压力较大的主库上增加从库
5. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
一个字符串类型的内存最大容量
512M
大量数据插入的处理
Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。
背景
原有模式客户端发送的操作命令为“一问一答”中间存在多次网络往返(网络开销大,导致吞吐量低)
工作模式
一次性发送多条命名,并在执行完后一次性打包返回
通过减少客户端与 redis 的通信次数来实现降低往返延时时间;
Pipeline 实现的原理是队列,而队列的原理是先进先出,这样就保证数据的顺序性。
Pipeline 的默认的同步的个数为53个,也就是说 arges 中累加到53条数据时会把数据提交
Pipeline 实现的原理是队列,而队列的原理是先进先出,这样就保证数据的顺序性。
Pipeline 的默认的同步的个数为53个,也就是说 arges 中累加到53条数据时会把数据提交
redis需要在所有命名执行完之前缓存所有命名的执行结果
具体能够容忍的操作个数和socket-output 缓冲区大小/返回结果的数据尺寸都有很大的关系;
同时也意味着每个 redis-server 同时所能支撑的 pipeline 链接的个数,也是有限的,这将受限于 server 的物理内存或网络接口的缓冲能力。
同时也意味着每个 redis-server 同时所能支撑的 pipeline 链接的个数,也是有限的,这将受限于 server 的物理内存或网络接口的缓冲能力。
查找指定模式的key
keys指令
速度快
由于redis单线程,数据量大时会导致服务阻塞
scan命令
速度慢
无阻塞获取指定模式Key,由于是非阻塞操作,因此会有不准确的情况(执行该命令期间会有别的命令还在执行)
延时队列的实现
使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。
分布式寻址算法
算法
hash slot(哈希槽) 算法(redis clustre采用)
hash 算法
比较适合固定分区或者分布式节点的集群架构
缺点
扩容或缩容会导致缓存重建
一致性 hash 算法(自动缓存迁移)
缺点
数据倾斜问题,无法控制节点分布的均匀性
一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
解决数据倾斜问题,为每一个设置多个虚拟节点(实际应用一般设置32甚至更大),使得数据分布均匀。
应满足的条件
平衡性
单调性
分散性
负载
平滑性
布隆过滤
判断一个元素是否在集合中存在
存在一定的误判率;
过滤器越长误判率越小;
哈希函数越多,效率越差,误判率越小
过滤器越长误判率越小;
哈希函数越多,效率越差,误判率越小
参考
详细解析Redis中的布隆过滤器及其应用
详解布隆过滤器的原理、使用场景和注意事项
常用工具
redisson
一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象
功能简单,不支持字符串操作,不支持排序,事务,管道,分区等特性
jedis
redis的java客户端,提供比较全的redis命令操作
lettuce
CRC16
通过异或操作来替代的一种除法取余操作
0 条评论
下一页