Redis第一讲:相关的基础知识
摘要:本文是Redis(6.2.1)详解的第一讲,介绍Redis相关的基础知识,内存存储和持久化,Redis作缓存使用时的注意要点,常见的数据类型,缓存的过期策略,Redis和数据库双写一致性的问题。作为常见的nosql数据库,Redis在项目中经常作为缓存来使用,更是面试中必备的技能
文章目录
- Redis第一讲:相关的基础知识
- 0、Redis 学习资料
- 1、为什么学习 Redis?
- 1、Redis 是什么?(远程字典服务器)
- 1.1、传统关系型数据库和nosql区别?(比较学习法)
- 1.2、Redis定义
- 1.3、为什么使用 Redis?
- 1.4、Redis的缺点?
- 2、Redis的应用
- 2.1、配合关系型数据库做高速缓存(核心)
- 2.2、多样的数据结构 - 用来存储数据
- 3、Redis为什么可以做缓存/Redis什么时候使用/保存的数据是什么/使用缓存的合理性问题?20181222
- 3.1、项目中使用 Redis的目的是什么?
- 3.2、Redis 作查询缓存时,需要注意考虑哪几个问题(防止脏读/序列化查询结果/为查询结果生成一个标识)
- 4、Redis 的特征?
- 5、Redis内部结构?
- 5.1、从开发者的角度
- 5.2、从内部实现的角度
- 5.3、内部实现为何这么设计?
- 5.4、dict 详解?
- 5.5、Sorted Set 底层数据结构
- 5.6、Sorted Set 为什么同时使用字典和跳跃表?
- 5.7、Sorted Set 为什么使用跳跃表,而不是红黑树?
- 5.8、Hash 对象底层结构
- 5.9、Hash 对象的扩容流程
- 5.10、渐进式 rehash 的优点
- 5.11、rehash 流程在数据量大的时候会有什么问题吗(Hash 对象的扩容流程在数据量大的时候会有什么问题吗)
- Action1:Redis的Hash表会被装满吗?装满了会怎么办?
- Action2:Redis 的字符串(SDS)和C语言的字符串区别是啥
- 6、Redis中各个数据类型的常用命令及使用场景?重点
- 6.1、String 字符串,最基础的数据类型
- 6.2、散列类型 hash
- 6.3、列表类型 list
- 6.4、集合类型 set
- 6.5、有序集合类型 zset
- 6.6、HyperLogLog
- 6.7、Geo 地理位置信息
- 6.8、Bitmap:位图
- 6.9、Stream
- 7、Redis 数据淘汰策略(LRU 算法,slab 槽分配),如何减少内存碎片 (面试官的问题的解决方案) 美团
- 8、Redis和数据库双写一致性的问题? **我的项目中没有双写的问题,因为redis只是用作缓存,哈哈 20181104
- 9、Redis键的过期时间
- 10、Redis数据的同步问题。更改了后台的数据(比如修改价格),而此数据在redis中保存着。 被新浪问到过
- 11、解析配置文件redis.conf (端口号6379)
- 12、Redis键值设计规范
- 13、命令使用规约
- 14、删除redis bigkey **(bigKey的问题)**
- 15、Redis的pfadd,pfcount?
- 16、一个 Redis 实例最多能存放多少的 keys? List、 Set、 Sorted Set 他们最多能存放多少元素? 美团
- 17、Redis使用规范
- 17.1、接入Redis缓存服务
- 17.2、使用规范
0、Redis 学习资料
参考的书籍
- 《Redis实战 黄建宏译》
- 《Redis使用手册 黄建宏译》
- 一本好的工具书,可以帮助我们快速地了解或查询 Redis 的日常使用命令和操作方法
- 《Redis 设计与实现 黄建宏》
- 这本书讲解得非常透彻,尤其是在 Redis 底层数据结构、RDB 和 AOF 持久化机制,以及哨兵机制和切片集群的介绍上
- 《Redis入门指南 李子骅》
- 《高并发编程网》
- 《尚硅谷的Redis资料》
- 《Redis 开发与运维》
- 针对 Redis 阻塞、优化内存使用、处理 bigkey 这几个经典问题,提供了解决方案
- 可以参考 Redis第二讲:Redis进阶与实战/持久化机制/分布式锁/Redis集群/设计模式/缓存击穿和缓存雪崩/事务机制
1、为什么学习 Redis?
为什么学习 Redis?
- 其特性在项目中大量被使用,让你在项目开发中更加游刃有余;
- Redis 在面试中重要程度上:在当前 Java 后端面试中,Redis所有框架/中间件中被问到频率是最高的
学习的建议:
- 1、阅读源码。读源码其实也是一种实战锻炼,可以帮助你从代码逻辑中彻底理解 Redis 系统的实际运行机制。当遇到问题时,可以直接从代码层面进行定位、分析和解决问题
- 2、亲自动手实践
- 在开发中,使用并体会其特性
1、Redis 是什么?(远程字典服务器)
1.1、传统关系型数据库和nosql区别?(比较学习法)
- rdbms:组织结构化数据;结构化查询语言;数据和关系都存储在单独的表中;严格的一致性
- 关系型数据库的缺点
- 1、关系数据库存储的是行记录,无法存储数据结构
- 以微博的关注关系为例,“我关注的人”是一个用户 ID 列表,使用关系数据库存储只能将列表拆成多行,然后再查询出来组装,无法直接存储一个列表。
- 2、关系数据库的 schema 扩展很不方便
- schema 是强约束,操作不存在的列会报错,业务变化时扩充列也比较麻烦,需要执行 DDL(data definition language,如 CREATE、ALTER、DROP 等)语句修改,而且修改时可能会长时间锁表
- 例如,MySQL 在对大批量数据进行操作时,可能锁表 1 个小时
- 3、关系数据库在大数据场景下 I/O 较高
- 如果对一些大量数据的表进行统计之类的运算,关系数据库的 I/O 会很高,因为即使只针对其中某一列进行运算,关系数据库也会将整行数据从存储设备读入内存中。
- 4、关系数据库的全文搜索功能比较弱
- 关系数据库的全文搜索只能使用 like 进行整表扫描匹配,性能非常低,在各种复杂的场景下无法满足业务要求。
- 例如:对品牌表中适用类目的的全文检索,导致慢查询性能问题
- 再如:模糊查询商品名称,会导致慢查询性能问题
- Nosql:代表的不仅仅是sql,它没有声明新的查询语言;使用键值对存储,实现了数据的最终一致性(非ACID)
- 常见的NoSql方案
- K-V 存储:解决关系数据库无法存储数据结构的问题,以 Redis 为代表。
- 例如:单台 Memcache 服务器,使用简单的 key - value 查询,能够达到 TPS 50000 以上
- 文档数据库:解决关系数据库强 schema 约束的问题,以 MongoDB 为代表。
- 文档数据库最大的特点就是 no-schema,可以存储和读取任意的数据。目前绝大部分文档数据库存储的数据格式是 JSON
- 列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表。
- 它一般将列式存储应用在离线的大数据分析和统计场景中
- 全文搜索引擎:主要解决关系数据库的全文搜索性能问题,以 ElasticSearch 为代表。
- ElasticSearch的使用和原理介绍可以参考这两篇文章:
1.2、Redis定义
- Redis 是 key-value 形式的 nosql 数据库,内存中的数据结构存储系统。可以用作数据库、缓存和消息中间件。Redis 的 Value 是具体的数据结构,包括 String(字符串)、hash(哈希类型)、list(链表)、set(集合)、sorted set(有序集合)、bitmap 和 hyperloglog,所以常常被称为数据结构服务器。
- 这些数据类型都支持 push/pop、add/remove 及取交集、并集和差集等更丰富的操作,而且这些操作都是原子性的。
- 在此基础上,Redis支持各种不同方式的排序
1.3、为什么使用 Redis?
- 在高并发场景下,如果经常需要连接结果变动频繁的数据库,会导致数据库读取及存取的速度变慢,数据库压力极大。因此我们需要通过缓存来减少数据库的压力,使得大量的访问进来能够命中缓存,只有少量的请求进入数据库层。由于缓存基于内存,因此可支持的并发量远远大于基于硬盘的数据库。
- 具体的量级如下:
- MySQL的量级是:
- Redis的量级是:
- 你们的系统用到的机器数量是多少?相比重构前减少了多少?
1.4、Redis的缺点?
- 它并不支持完整的 ACID 事务,只能保证隔离性和一致性(I 和C),无法保证原子性和持久性(A 和 D)。
不适用的场景
- Redis不太适合有的key的value特别大,因为这种情况下,会导致整个 Redis 查询速度变慢。
- 具体的案例如下 todo
2、Redis的应用
2.1、配合关系型数据库做高速缓存(核心)
- 对于高频次、被热门访问的数据,使用 Redis 做缓存,可以降低数据库I/O
- 可以查看下图中示例
- 在分布式架构中,使用 Redis 做 session 共享
2.2、多样的数据结构 - 用来存储数据
- 取最新发商品的前N个类目,放在Redis的List集合里面;
- 分布式锁(set + lua 脚本)
- 计数器:读取类目的计数逻辑
- 排行榜( zset )
- 消息队列(stream)
- 地理位置(geo)
- 访客统计(hyperloglog)
- 具体实现详情,可以参考第六节的内容
我们在项目中用到的:首页的访问量是整个系统中最大的,所以需要使用缓存来减轻读取数据库的压力。对于首页商品的广告位展示, 使用了 Redis 保存 hash 类型的数据。 当商品内容添加时,直接添加到数据库中;用户请求时,先调用服务层查询首页内容,然后查询 Redis,如果没有查询到数据,在从MySQL 数据库中查询内容分类信息,同时把查询结果写入 Redis 中。(如何写:查询的是图书item对象,使用JsonUtils工具类把对象转成json字符串)
对首页内容修改时,我们先更新 MySQL数据库,然后调用服务层,清除 Redis 中相对应 key 的内容。
- 默认16个数据库,类似数组下标从0开始,初始默认使用0号库
- 使用命令
select < dbid>
来切换数据库。如: select 8 -
dbsize
查看当前数据库的key的数量 -
flushdb
清空当前库 -
flushall
通杀全部库
3、Redis为什么可以做缓存/Redis什么时候使用/保存的数据是什么/使用缓存的合理性问题?20181222
3.1、项目中使用 Redis的目的是什么?
- 1、Redis 将所有数据存入内存中,因此存取速度非常快,能减轻数据库的压力,提高存取的效率,在互联网项目中,只要是涉及高并发或者是存在大量读数据的情况下都可以使用 Redis 作为缓存。
- 2、Redis 提供丰富的数据类型,除了缓存还可以根据实际的业务场景来决定 Redis 的作用。
- 例如:使用 Redis 保存用户的购物车信息、生成订单号、访问量计数器、任务队列、排行榜等。
- 3、Redis不仅能保存 String 类型的数据,还能保存Lists(有序)/ Sets类型(无序) 类型的数据,而且还能完成排序(SORT) 等高级功能,在实现INCR,SETNX等功能的时候,保证了其操作的原子性。除此以外,还支持主从复制等功能。
3.2、Redis 作查询缓存时,需要注意考虑哪几个问题(防止脏读/序列化查询结果/为查询结果生成一个标识)
- 1、防止脏读
- 我们缓存了查询结果,那么一旦数据库中的数据发生变化,缓存的结果就不可用了。为了实现这一保证,可以在执行相关表的更新查询(update,delete,insert)查询后,让相关的缓存过期。这样下一次查询时,程序会重新从数据库中读取最新数据,并缓存到 Redis 中。
- Q: 在执行一条 insert 前我怎么知道应该让哪些缓存过期呢?
- A: 对于Redis,我们可以使用 Hash 结构,让一张表对应一个 Hash,所有在这张表上的查询都保存到该 Hash 下,这样当表数据发生变动时,直接设置过期即可。我们可以自定义一个注解,在数据库查询方法上通过注解的属性注明这个操作与哪些表相关,这样在执行过期操作时,就能直接从注解中得知应该让哪些 Set 过期了
//表示该方法需要执行 (缓存是否命中? 返回缓存并阻止方法调用:执行方法并缓存结果)的缓存逻辑
@RedisCache(type = JobPostModel.class)
JobPostModel selectByPrimaryKey(Integer id);
//表示该方法需要执行清除缓存逻辑
@RedisEvict(type = JobPostModel.class)
int deleteByPrimaryKey(Integer id);
- 2、序列化查询结果
- 利用JDK自带的ObjectInputStream/ObjectOutputStream,将查询结果序列化成字节序列,即需要考虑 Redis 的实际存储问题
- 也可以使用第三方工具包jackson,protostuff,json字符串化
- 3、为查询结果生成一个标识
- 被调用的方法所在的类名、方法名、该方法的参数三者共同标识一条查询结果。也就是说,如果两次查询调用的类名、方法名和参数值相同,我们就可以确定这两次查询结果一定是相同的(当然是在数据没有变动的前提下)。因此,我们可以将这三个元素组合成一个字符串做为key,解决标识问题
- 4、以 AOP 方式使用 Redis
- 方法被调用之前,根据类名、方法名和参数值生成Key;然后通过 Key 向 Redis 发起查询请求;如果缓存命中,则将缓存结果反序列化作为方法调用的返回值,并将其直接返回;如果缓存未命中,则继续向数据库中查询,并将查询结果序列化存入 Redis 中,同时将查询结果返回。
- 案例如下:例如,插入数据时,删除缓存逻辑如下:
/* 在方法调用前清除缓存,然后调用业务方法
* @param jp
* @return
* @throws Throwable */
@Around("execution(* com.zjut.dao.mapper.JobPostModelMapper.insert*(..))" +
"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.update*(..))" +
"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.delete*(..))" +
"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.increase*(..))" +
"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.decrease*(..))" +
"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.complaint(..))" +
"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.set*(..))")
public Object evictCache(ProceedingJoinPoint jp) throws Throwable {}
4、Redis 的特征?
1、单线程
- 利用 Redis 的队列技术将访问变为串行访问,消除了传统数据库串行控制的开销
2、完全基于内存,存取速度极快
3、Redis的线程模型
- 做消息队列时会用到线程模型,使用了多路I/O复用模型
- 具体而言:多路复用I/O模型利用 epoll机制(linux操作系统提供,是select/poll 的增强版)它可以同时监视多个流的I/O事件,在空闲时,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,线程就会从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll只轮询哪些真正发出了事件的流),并且只依次顺序处理就绪的流,这种做法就避免了大量的无用操作。针对文件字节/字符操作时:使用的是面向缓冲区 (传统上是使用面向流
5、Redis内部结构?
5.1、从开发者的角度
- Redis 支持五种数据类型存储
- 1.string字符串
- 2.hash散列
- 3.list列表(排序的双向链表)
- 4.set集合
- 5.zset有序集合 //之后版本补充的数据类型有hyperloglog geo等
5.2、从内部实现的角度
- ht(dict: 用于维护 key 和 value 映射关系的数据结构,与 map 和 dictionary 类似,在 hash结构中,当他的 filed 较多时,便会采用 dict 来存储,Redis 配合使用 dict 和 skiplist 来共同维护一个 sorted set) raw,embstr,intset,sds,ziplist,quicklist
- skiplist(跳表)
5.3、内部实现为何这么设计?
- 存储效率,快速响应时间,单线程
5.4、dict 详解?
- dict 是一个基于 hash 表的算法,它采用某个哈希函数从 key 计算得到其在哈希表中的位置,并采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing),rehash 时每次只对一小部分进行 rehash 操作。
- Redis 内部使用一个redisObject 对象来标识所有的 key 和 value 数据,
- redisObject 最主要的信息如图所示:
- type 代表一个 value 对象具体是何种数据类型
- encoding 是不同数据类型在 Redis 内部的存储方式
- 比如:type = string 代表 value 存储的是一个普通字符串,那么对应的encoding可以是 raw或是 int,如果是 int 则代表 Redis 内部是按数值类型存储和表示这个字符串。
- 左边的 raw 列为对象的编码方式(底层实现)
- 字符串可以被编码为 raw(一般字符串)或Rint(为了节约内存,Redis会将字符串表示的64位有符号整数编码为整数来进行储存);
- 列表可以被编码为 ziplist 或 linkedlist
- ziplist 是为了节约大小较小的列表空间而作的特殊表示;
- 集合可以被编码为 intset 或者 hashtable
- intset 是只储存数字的小集合的特殊表示;
5.5、Sorted Set 底层数据结构
- 有序集合(Sorted Set) 可以被编码为 ziplist 或者 skiplist 格式,
- ziplist使用压缩列表实现,用于表示小的有序集合,而 skiplist 则用于表示任何大小的有序集合。
- 压缩列表( ziplist) 本质上就是一个字节数组, 是 Redis 为了节约内存而设计的一种线性数据结构, 可以包含多个元素, 每个元素可以是一个字节数组或一个整数;
- 使用时机:
- 数据量小时用压缩列表,当保存的元素长度都小于64字节,同时数量小于128时,使用该编码方式
- 这两个参数可以通过
zset-max-ziplist-entries
、zset-max-ziplist-value
来自定义修改
- 跳跃表( skiplist) 是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。 跳跃表支持平均 O(logN) 、 最坏 O(N)复杂度的节点查找, 还可以通过顺序性操作来批量处理节点。
- 如下图所示:zset实现,一个zset同时包含一个字典(dict)和一个跳跃表(zskiplist)
5.6、Sorted Set 为什么同时使用字典和跳跃表?
主要是为了提升性能。
- 单独使用字典:在执行范围型操作,比如 zrank、zrange,字典需要进行排序,至少需要 O(NlogN) 的时间复杂度及额外 O(N) 的内存空间。
- 单独使用跳跃表:根据成员查找分值操作的复杂度从 O(1) 上升为 O(logN)。
5.7、Sorted Set 为什么使用跳跃表,而不是红黑树?
主要有以下几个原因:
- 1)跳表的性能和红黑树差不多。
- 2)跳表更容易实现和调试。
5.8、Hash 对象底层结构
- hash 可以编码为 ziplist 或者 hashtable
- ziplist:使用压缩列表实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的节点推入到压缩列表的表尾,然后再将保存了值的节点推入到压缩列表表尾。
- 因此:1)保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;2)先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加的会被放在表尾方向。
- hashtable:使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值来保存,跟 java 中的 HashMap 类似
5.9、Hash 对象的扩容流程
hash 对象在扩容时使用了一种叫“渐进式 rehash”的方式,步骤如下:
- 1)计算新表 size、掩码,为新表 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 2)将 rehash 索引计数器变量 rehashidx 的值设置为0,表示 rehash 正式开始。
- 3)在 rehash 进行期间,每次对字典执行添加、删除、査找、更新操作时,程序除了执行指定的操作以外,还会触发额外的 rehash 操作,在源码中的
_dictRehashStep
方法。
- _dictRehashStep:从名字也可以看出来,大意是 rehash 一步,也就是 rehash 一个索引位置。
- 该方法会从 ht[0] 表的 rehashidx 索引位置上开始向后查找,找到第一个不为空的索引位置,将该索引位置的所有节点 rehash 到 ht[1],当本次 rehash 工作完成之后,将 ht[0] 索引位置为 rehashidx 的节点清空,同时将 rehashidx 属性的值加一。
- 4)将 rehash 分摊到每个操作上确实是非常妙的方式,但是万一此时服务器比较空闲,一直没有什么操作,难道 redis 要一直持有两个哈希表吗?
- 答案当然不是的。我们知道,Redis 除了文件事件外,还有时间事件,Redis 会定期触发时间事件,这些时间事件用于执行一些后台操作,其中就包含 rehash 操作:当 Redis 发现有字典正在进行 rehash 操作时,会花费1毫秒的时间,一起帮忙进行 rehash。
- 5)随着操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 至 ht[1],此时 rehash 流程完成,会执行最后的清理工作:释放 ht[0] 的空间、将 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx 的值为 -1。
5.10、渐进式 rehash 的优点
- 渐进式 rehash 的好处在于它采取分而治之的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 而带来的庞大计算量。
- 在进行渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间,字典的删除、査找、更新等操作会在两个哈希表上进行。例如,要在字典里面査找一个键的话,程序会先在 ht[0] 里面进行査找,如果没找到的话,就会继续到 ht[1] 里面进行査找,诸如此类。
- 另外,在渐进式 rehash 执行期间,新增的键值对会被直接保存到 ht[1], ht[0] 不再进行任何添加操作,这样就保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。
5.11、rehash 流程在数据量大的时候会有什么问题吗(Hash 对象的扩容流程在数据量大的时候会有什么问题吗)
- 1)扩容期开始时,会先给 ht[1] 申请空间,所以在整个扩容期间,会同时存在 ht[0] 和 ht[1],会占用额外的空间。
- 2)扩容期间同时存在 ht[0] 和 ht[1],查找、删除、更新等操作有概率需要操作两张表,耗时会增加。
- 3)Redis 在内存使用接近 maxmemory 并且有设置驱逐策略的情况下,出现 rehash 会使得内存占用超过 maxmemory,触发驱逐淘汰操作,导致 master/slave 均有有大量的 key 被驱逐淘汰,从而出现 master/slave 主从不一致。
Action1:Redis的Hash表会被装满吗?装满了会怎么办?
ReHash的过程如上所述
- 潜在的影响
- ReHash时会对Redis的读写有何影响
Action2:Redis 的字符串(SDS)和C语言的字符串区别是啥
C字符串 | SDS |
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本数据或者二进制数据 |
可以使用所有的<string.h>库中的函数 | 可以使用一部分<string.h>库中的函数 |
6、Redis中各个数据类型的常用命令及使用场景?重点
- 缓存(核心)
- 商品中心对缓存的使用,可以看这篇文章 项目实战第二讲:高并发下如何保障缓存和数据库的一致性
- 分布式锁(set + lua 脚本)
- 可以看这篇文章第16节 Redis第二讲:Redis进阶与实战/持久化机制/分布式锁/Redis集群/设计模式/缓存击穿和缓存雪崩/事务机制
- 排行榜(zset)
- 计数(incrby)
//如果key不存在,那么key的值会先被初始化为0,然后再执行INCRBY命令
jedisClientUtil.incrBy(key, 1);
final Long excuteCount = jedisClientUtil.hincrBy(hashKey, ScanResult.RESULTKEY_FINISHED, excuteSize);
- 消息队列(stream)
- 地理位置(geo)见6.7节
- 访客统计(hyperloglog)
6.1、String 字符串,最基础的数据类型
- Redis 的 String 可以包含任何数据,比如jpg图片或者序列化的对象,value最大可以为512M
- 赋值与取值方法
set/get
// get
String key = GET_OTHER_SET_INTERFACE_PREFIX + Digestors.buildMd5(ImJsonUtils.objToJson(requestDTO));
String value = jedisClientUtil.get(key);
// set
jedisClientUtil.set(categoryInstanceKey, cacheData);
- 递增数字
incr
- 存储的字符串为整型时可以使用
- 例如:对文章访问量统计,可以使用
incr
自增;
// 判断调用接口是否超过限制
Long count = jedisClientUtil.incr(limit.getCacheKey());
String count = RedisKeyGenerator.BATCH_GOODS_CREATE_UNLOCK_COUNT_KEY + message.getRequestId();
Long resultLength = jedisClientUtil.incr(count);
// 然后进行后续判断
- 减少数字
decr
- 存储的字符串为整型时可以使用
private BatchApplyAuditThreadPool() {
super(DEFAULT_THREAD_SIZE, DEFAULT_THREAD_SIZE, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(DEFAULT_QUEUE_SIZE),
r -> new Thread(r,
NAME + "-" + threadNum.getAndDecrement()), new RewriteCallerRunsPolicy());
}
- 位操作:
getbit
- 获取字符串指定位置的二进制位的值
- 在项目中暂时没有使用场景
- 判断字符串是否存在:
exists
// 使用jedisClientUtil存放后台类目树json数据
if(jedisClientUtil.exists(RedisKeyPrefixConstants.BACK_CATEGORY_TREE)){
return Response.ok(RedisKeyPrefixConstants.BACK_CATEGORY_TREE);
}
- String 字符串实践:
-
setex(set with expire)
键秒值
// setex 设置缓存key过期时间
jedisClientUtil.setex(RedisKeyConstants.UPDATE_AG_CATEGORY+ "configId="+ configId, TIMEOUT_SECOND, String.valueOf(System.currentTimeMillis()));
setnx(set if not exist)
//setex:设置带过期时间的key,可以动态设置
- setnx:只有在key不存在时才设置key的值;
// 设置缓存过期时间
Long setnx = jedisClientUtil.setnx(String.format(IDEMPOTENT_KEY, categoryId), String.valueOf(categoryId));
mset/mget/msetnx
-
mset
同时设置一个或多个key-value对
jedisClientUtil.hmset(redisKey, initInfoMap(total));
jedisClientUtil.expire(redisKey, Objects.isNull(expTime) ? DEFAULT_TIME : expTime);
private Map<String,String> initInfoMap(Long total){
Map<String,String> map = Maps.newHashMap();
if(Objects.nonNull(total)){
map.put(TOTAL_KEY,String.valueOf(total));
}
map.put(SEND_KEY,INIT_NUM);
map.put(END,NO_FINISH);
return map;
}
mget
- 获取所有给定key的值;
String[] keys = paramIds.stream().map(param -> keyPrefix + param).toArray(String[]::new);
//先读缓存
List<String> values = jedisClientUtil.mget(keys);
-
msetnx
:当所有给定key不存在时,设置key-value对; getset
:先get返回旧值,然后立即set
- 目前在项目中没有使用
6.2、散列类型 hash
是一个 String 类型的 field 和 value 的映射表,hash 特别适用于存储对象
- 1、Hash 命令都与String键相似,存在的意义是啥?
- ①hash 键可以将信息凝聚在一起,而不是直接分散存储在整个Redis中,可以避免键名冲突;
- ②减少内存占用
- 最重要的作用
- 2、什么时候不适合使用 hash 类型存储数据?
- 1、有过期功能的使用,因为过期功能只能使用在 key 上;
- 2、二进制操作命令,如setbit,getbit,bitop;
- 3、需要考虑 数据量分布的问题 ,集群方案中,有分配槽的概念,使用crc16算法 --》将结果分成%16384 进行数据槽分配,对 key 进行 crc16运算,得到的结果都会分配到同一个槽中,导致数据分配不均;
- 常用命令:
- 赋值与取值
hset/hget
// 设置缓存,入参为key,field,value
jedisClientUtil.hset(cacheKeys[0],
Arrays.stream(point.getArgs())
.skip(1)
.map(String::valueOf)
.collect(Collectors.joining("")), ImJsonUtils.objToJson(result));
// 设置缓存,入参为key,field,value
jedisClientUtil.hset(cacheKeys[0], (String) point.getArgs()[1], ImJsonUtils.objToJson(result));
// 获取缓存,入参为key,field
String result = jedisClientUtil.hget(cacheKeys[0],
Arrays.stream(point.getArgs())
.skip(1)
.map(String::valueOf)
.collect(Collectors.joining("")));
- 判断字段是否存在
hexists
// 判断字段是否存在,入参为 key,field
if (jedisClientUtil.hexists(message.getResponseKey(), String.valueOf(channelItemId))) {
log.info("重复消息({}-{}),丢弃", message.getResponseKey(), channelItemId);
return ZMQConsumeStatus.CONSUME_SUCCESS;
}
- 增加数组
hincrby
// 入参为 key、field、value,对SEND_KEY对应的value加上 id.size()
return jedisClientUtil.hincrBy(redisKey, SEND_KEY, id.size());
// 2、重置子任务总数 对CHILDREN_PUSH_NUM对应的value加上childrenJobs.size() 子任务可能与主任务中 totalJob 不等
jedisClientUtil.hincrBy(redisKey, RedisKeyConstants.CHILDREN_PUSH_NUM, childrenJobs.size());
- hash 实践:
- 1、统计几亿用户系统的签到,去重登录次数统计,用户是否在线的状态 setbit、getbit、bitcount命令,
- 原理是:Redis内构件一个足够长的数组,每个数组元素只能是0和1两个值 数组的下标index用来表示我们上面例子里面的用户id
- 在我们项目中没有这种使用场景
- 2、hash实现幂等性请求,可以验证前端的重复请求,通过 Redis 进行过滤;在每次请求时,将request ip,参数,接口等 hash 作为 key 存储到 Redis,然后设置有效期,下次请求过来时先在 Redis 中检索有没有这个 key,从而验证是不是一定时间内过来的重复提交。
- 具体实践可以看这篇文章:项目实战第二十四讲:商品订单系统,如何保证数据准确无误
6.3、列表类型 list
按照插入顺序排序,可以添加元素到列表的头部或尾部
- 常用命令:
- 增加元素
lpush/rpush
//从DB中获取的信息id放入到redis
String key = BaseConstants.INSTANCE_CATEGORY_ID_PREFIX + searchDTO.getInstanceCode();
jedisClientUtil.rpush(key, cacheContent);
// 缓存前台类目
String pidKey = BaseConstants.FRONT_CATEGORY_PID_KEY_PREFIX + pid;
List<FrontCategory> result = frontCategoryDao.findChildren(pid);
if (CollectionUtils.isEmpty(result)) {
jedisClientUtil.rpush(pidKey, BaseConstants.CACHE_NO_DATA);
return Collections.emptyList();
}
String[] cacheContent = result.stream().map(FrontCategory::getId).map(String::valueOf).toArray(String[]::new);
//从DB中获取的信息id放入到redis
jedisClientUtil.rpush(pidKey, cacheContent);
- 从哪个列表两端弹出元素
lpop/rpop
- 可以使用 list 类型模拟栈 lpush+lpop 和队列 lpush+rpop 的操作
// 商品发布频率限制
// llen 返回列表长度
Long llen = jedisClientUtil.llen(frequencyKey);
Long rTime = null;
// 拿到窗口最右边时间
for (int i = 0; i <= llen - limitNum; i++) {
try {
String rpop = jedisClientUtil.rpop(frequencyKey);
// 降级,场景:配置限制数量调小一瞬间,机构多用户并发下队列可能清空
if (StringUtils.isEmpty(rpop)) {
return;
}
rTime = Long.parseLong(rpop);
} catch (Exception e) {
log.warn("[商品发布频率限制]redis频率key取值失败,场景:{},frequencyKey:{},cause:", scenes, frequencyKey, e);
}
}
- 获取列表片段
lrange
删除lrem
//先读缓存,入参为 key,开始索引和结束索引
List<String> idStrings = jedisClientUtil.lrange(key, 0, -1);
if(CollectionUtils.isEmpty(idStrings)){
return null;
}
if(idStrings.size() == 1 && BaseConstants.CACHE_NO_DATA.equals(idStrings.get(0))){
return Lists.newArrayList();
}
return idStrings.stream().map(NumberUtils::toLong).collect(Collectors.toList());
Long len = jedisClientUtil.llen(requestKey);
List<String> dataStr = jedisClientUtil.lrange(requestKey, 0, len);
if (CollectionUtils.isEmpty(dataStr)) {
log.warn("批量任务还未开始!!,requestKey: {}", requestKey);
return Response.ok(null);
}
- 实践:
- 1、消息队列
- 可以见这篇文章:Redis第五讲:Redis在商品中心的应用
- 2、阻塞队列
6.4、集合类型 set
特点:无序,不允许重复
- 命令:
- 增加 / 删除
sadd/srem
public String addRepeatKeyText(Long categoryId, String KeyText, String bizType){
// 使用类目id和业务类型查询redis key
String redisKey = getTextKey(categoryId,bizType);
jedisClientUtil.sadd(redisKey,KeyText);
return redisKey;
}
// sadd
String redisKey = getKey(categoryId,bizType);
String redisSetKey = getIdSetKey(redisKey);
if(!jedisClientUtil.exists(redisKey)){
return 0L;
}
String[] ids = longList2Str(id,childType);
jedisClientUtil.sadd(redisSetKey,ids);
return jedisClientUtil.hincrBy(redisKey,SEND_KEY,id.size());
private void saveToRedis(String instance, Map<Long, Set<String>> map) {
map.forEach((segment, idSet) ->
jedisClientUtil.sadd(getRedisKey(instance, segment), idSet.toArray(new String[0])));
}
private String getRedisKey(String instance, long segment) {
if (!StringUtils.hasText(instance)) {
instance = DEFAULT_INSTANCE;
}
return RedisKeyConstants.ID_CACHE.concat(CACHE_KEY_TIME).concat(":")
.concat(instance).concat(":")
.concat(String.valueOf(segment));
}
// srem
String redisSetKey = getIdSetKey(redisKey);
if (!jedisClientUtil.exists(redisKey)) {
return Boolean.FALSE;
}
String[] ids = longList2Str(id);
jedisClientUtil.srem(redisSetKey, ids);
/** 移除token */
private void removeToken(HttpServletRequest request, String token) {
try {
String sessionId = UserUtil.getSessionId();
jedisClientUtil.srem(generateKey(sessionId), token);
} catch (Exception e) {
// 此处redis不可用, 牺牲防重校验以保证业务正常进行
log.warn("redis is disabled", e);
}
}
- 获取集合中的所有元素
smembers
/**
* 清空已过公示期的品牌
*/
public void clearFrozenBrand() {
// 查询全量品牌信息
Set<String> frozenBrands = jedisClientUtil.smembers(ZCY_FROZEN_BRANDS);
if (CollectionUtils.isEmpty(frozenBrands)) {
return;
}
frozenBrands.forEach(frozenBrand -> {
BrandFrozenDto brandFrozenDto = ImJsonUtils.jsonToObj(frozenBrand, BrandFrozenDto.class);
// 超过公示期,删除缓存
if (brandFrozenDto.getNoticeExpire().before(new Date())) {
jedisClientUtil.srem(ZCY_FROZEN_BRANDS, frozenBrand);
}
});
}
// 批量上redis锁,防止多扣少扣
String redisKey = RedisKeyConstants.STOCK_BATCH_IN_OUT + ":batch:" + batchNo;
Set<String> redisLocks = jedisClientUtil.smembers(redisKey);
if (CollectionUtils.isEmpty(redisLocks)) {
redisLocks = Sets.newHashSetWithExpectedSize(waitOperateSkuIds.size());
}
- 判断元素是否在集合中
sismember
/**
* 主键重复数据是否已存在
* @param categoryId
* @param KeyText
* @param bizType
* @return
*/
public Boolean keyTextExits(Long categoryId,String KeyText,String bizType){
String redisKey = getTextKey(categoryId,bizType);
if(!jedisClientUtil.exists(redisKey)){
return Boolean.FALSE;
}
return jedisClientUtil.sismember(redisKey,KeyText);
}
- 集合间运算(差集sdiff sinter交集 sunion并集)
- 差集sdiff 在项目中没有使用
- sinter求交集,在项目中没有使用
- sunion求并集,在项目中没有使用
- 从集合中弹出一个元素
spop
/**
* 随机获取一个主键重复的数据
* @param categoryId
* @param bizType
* @return
*/
public String getRepeatKeyText(Long categoryId, String bizType){
String redisKey = getTextKey(categoryId,bizType);
if(!jedisClientUtil.exists(redisKey)){
return null;
}
String key = jedisClientUtil.spop(redisKey);
return key;
}
- 获取集合中元素个数
scard
String redisSetKey = getIdSetKey(redisKey);
Long count = jedisClientUtil.scard(redisSetKey);
- 进行集合运算并将结果存储
sdiffstore
- 在项目中没有使用
- 随机获取集合中的元素
srandmember
- 在项目中没有使用
- Set 实践:
- 1、可能认识的人(实现关注模型)
- A{1,2,3} B{2,3,4} C{4,6,7} 共同关注的人–》交集 可能认识的人–》差集 ;
- 2、实现电商商品的筛选
- 入库时会遍历他的静态标签列表,品牌,尺寸,处理器,内存 --》交集;
- 3、基于集合键运算,实现人和支付系统的对账
- 订单系统产生的数据,支付系统产生的数据 -》差集;
- 4、基于集合键,实现直播刷礼物,转发微博等抽奖活动
- sadd key(userId)//刷礼物,转发微博加到集合中,
- smembers key;//获取所有用户,
- spop key【count】 //抽取count个中奖者;
- srandmember key【count】//抽取count个中奖者;
- 5、基于集合键,实现点赞,签到,like等功能
- sadd //点赞,
- srem //取消点赞,
- sismember //检查用户是否点过赞
- smembers //获取点赞用户列表;
- 6、基于集合键,实现Tag功能 豆瓣
- sadd 添加标签,srem 移除标签
6.5、有序集合类型 zset
特点:底层使用跳跃表做了分值字段 且分值越大,集合的下标越小 时间复杂度为0(n)
- 命令:
- 增加
zadd
- 在项目中没有使用
- 获取元素的分数
zscore key member
- 在项目中没有使用
- 获取排名在某个范围的元素列表
zrange
{分数相同则比较字符串 时间复杂度为0(logn+m)}
- 在项目中没有使用
- 计算有序集合的交集
zinterstore
- 在项目中没有使用
- 实践:
- 1、基于有序集合键,实现自动补齐功能
- 这与用户使用量有关,先对用户的输入做分词处理,后面使用 zinterstore 计算交集 调用ajax 来查找;
- 2、实现单日排行榜(今日热搜 搜索热点)
- zadd/zincr/zrevrange;
- 大厅应该有这样的需求,商品中心目前没有
- 3、实现周,月,年的排行榜
- 在单日热点的基础上做交集运算 zunionstore zrevrange
6.6、HyperLogLog
通常用于基数统计
- 使用少量固定大小的内存,来统计集合中唯一元素的数量。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计。
6.7、Geo 地理位置信息
Geo:redis 3.2 版本的新特性。
- 可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作:获取2个位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合
6.8、Bitmap:位图
暂无使用场景
6.9、Stream
主要用于消息队列,类似于 kafka,可以认为是 pub/sub 的改进版。提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
- 在项目中没有使用场景
7、Redis 数据淘汰策略(LRU 算法,slab 槽分配),如何减少内存碎片 (面试官的问题的解决方案) 美团
- 1、Redis 的过期策略和内存淘汰机制。Redis 存的数据超出内存值,是如何删除过多的数据的?
- Redis 采用的是定期删除+惰性删除策略。定时器ttl负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗cpu资源。
- 定期删除+惰性删除工作机制–定期:Redis默认每隔100ms检查,有过期的key则删除。Redis不是每隔100ms将所有的key检查一次,而是随机抽取进行检查。如果只是采取定时删除策略,会导致很多key到时间没有删除。
- 2、存在的问题:
- 如果定时删除没有删除掉key,并且也没及时请求key,也就是惰性删除也没生效,这样redis的内存会越来越高,这时:应该采用内存淘汰机制。
- 3、内存淘汰方案
redis.conf中有一行配置:
#maxmemory-policy
volatile-lru //在设置了过期时间的key中,移除最近最少使用的key 不推荐
volatile-ttl //从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random //从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru //内存不足以容纳新的写入数据时,在键操作中,移除最近最少使用的key **推荐**
allkeys-lfu //在所有的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增
allkeys-random //内存不足以容纳新的写入数据时,在键操作中,随机移除某个key
noeviction // 默认策略 内存不足以容纳新的写入数据时,新写入操作会报错
- LRU 详解
- 最近最少使用策略 LRU( Least Recently Used) 是一种缓存淘汰算法, 是一种缓存淘汰机制。
- 它使用双向链表实现的队列,队列的最大容量为缓存的大小。在使用过程中,把最近使用的页面移动到队列头,最近没有使用的页面将被放在队列尾的位置;
- 使用一个哈希表,把页号作为键,把缓存在队列中的节点的地址作为值,只需要把这个页对应的节点移动到队列的前面,如果需要的页面在内存中,此时需要把这个页面加载到内存中,简单的说,就是将一个新节点添加到队列前面,并在哈希表中跟新相应的节点地址,如果队列是满的,那么就从队尾移除一个节点,并将新节点添加到队列的前面。
8、Redis和数据库双写一致性的问题? **我的项目中没有双写的问题,因为redis只是用作缓存,哈哈 20181104
1、对于一致性要求高的场景,实现同步方案,即查询redis,若查询不到再从DB查询,保存到redis;更新redis时,先更新数据库,然后将redis内容设置为过期(不要去更新缓存内容,直接设置缓存过期),再用ZINCRBY增量修正redis数据;
2、并发程度高的,采用异步队列的方式,采用kafka等消息中间件处理消息生产和消费;
3、阿里的同步工具canal,实现方式是模拟mysql slave和master的同步机制,监控DB binlog的日志更新来触发redis的更新,解放程序员的双手,减少工作量
4、利用mysql触发器API进行编程,c/c++语言是基础,学习成本高
redis新数据定时同步到数据库过程:
1、定时任务定时同步redis与数据库的数据,数据库里存储着演示数据,通过数据库的数据与redis对比,得出需要更新的数据;
2、在更新过程中,redis的数据还在增长,a、需要先读redis的数据,记下时间;b、再查询指定时间段里的数据库的数据;c、再用ZINCRBY增量修正redis数据,而不是直接用哪个ZADD覆盖redis数据
一致性分为强一致性和最终一致性:数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性的要求,就不能放缓存。使用了redis,只能保证最终一致性。采取正确的更新策略,先更新数据库,再删缓存,例如:对商品库存进行操作时,先更新数据库中的信息,然后删除缓存;其次:因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
不同情况下的常用处理方法:
- 1、简单逻辑处理:
1、增 db–cache:数据库插入成功,添加到缓存
2、删:cache–》db:先删缓存,再删数据库
3、改:db–》cache:先改数据库,再改缓存
4、查:cache–>db–>cache:先查缓存,没有查数据库,然后添加进缓存
- 2、复杂逻辑处理:
一个service中有多次数据库交互,并且由于spring事务传播性不同有时只在最外层事务提交时提交(spring默认事务传播性)
service(事务)–service/dao----cache
增:db:只插入数据库
删:cache–>db:先删缓存,再删数据库
改:cache–>db:先删缓存,再删数据库
查:cache–>db–>cache:先查缓存,没有则查库添加到缓存
9、Redis键的过期时间
作用:清除缓存数据 可以按小时(用户的登录信息),按天(商品详情页面的缓存),为键设置过期时间,过期后没自动删除该键;对于hash表这种数据类型,只能为整个键(field)设置过期时间,而不能为键里面的单个元素设置过期时间.
10、Redis数据的同步问题。更改了后台的数据(比如修改价格),而此数据在redis中保存着。 被新浪问到过
用 ActiveMQ 的消息传递来进行解耦。当我们在后台进行了商品数据的更新后,就利用 MQ 发送一个消息出去,比如说消息内容是商品 id,然后在缓存模块中监听该消息,拿到商品id后到缓存中删除该商品数据就行了。
前(redis)后(mysql)台数据的同步
方案1:读:读缓存redis,没有的话,读mysql,并将mysql的值写入到redis;写:写mysql,成功后,再删除redis中对应的缓存。
方案2:使用RDB快照技术+AOF增量技术(redis变更的数据)。同步对redis的写操作命令到日志文件,然后同步到mysql数据库中。 最终一致性
11、解析配置文件redis.conf (端口号6379)
includes里面包括
daemonize 设置为yes可以启动守护线程 此时redis把pid写入var/run/redis.pid中
tcp-keepalive syslog-enabled是否把日志输出到syslog中
snapshotting快照配置 save写操作次数 RDB是整个内存的压缩过的snapshot,RDB的数据结构,可以配置复合的快照触发机制
limits限制 可以设置内存淘汰机制
append only mode追加配置(always everysec:每秒同步一次 no:等操作系统进行数据缓存同步到磁盘)
vm-enabled no 指定是否启动虚拟内存机制 默认为no,即VM机制将数据分页存放,由redis将访问量较少的页(冷数据)swap到磁盘上,访问多后会从磁盘换出到内存
12、Redis键值设计规范
参考ali redis规约
1、key名设计【建议】
- 可读性和可管理性 以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
- item-standard:prop_attr:1
2、key名设计【建议】
- 简洁性 保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视
- 例如:user:{uid}:friends:messages:{mid} =>简化为u:{uid}: fr: m:{mid}。
3、key名设计【强制】
- 不要包含特殊字符 反例:包含空格、换行、单双引号以及其他转义字符
4、value设计【强制】
- 拒绝bigkey(防止网卡流量、慢查询) string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
- 反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),查找方法和删除方法
5、value设计【推荐】
- 选择适合的数据类型。
- 例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡)
- 反例:set user:1:name tom set user:1:age 19 set user:1:favor football
- 正例: hmset user:1 name tom age 19 favor football
6、value设计【推荐】
- 控制key的生命周期,redis不是垃圾桶
- 建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime(得到键多久没有被访问到的秒数)
13、命令使用规约
序号 | 内容 | 示例 |
1 | 【推荐】 O(N)命令关注N的数量 | 例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。 |
2 | 【推荐】:禁用命令 | 禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。 |
3 | 【推荐】合理使用select | redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。 |
4 | 【推荐】使用批量操作提高效率原生命令 | 例如mget、mset。 非原生命令:可以使用pipeline提高效率。 但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。注意两者不同:(1). 原生是原子操作,pipeline是非原子操作。(2). pipeline可以打包不同的命令,原生做不到 (3). pipeline需要客户端和服务端同时支持。 |
5 | 【建议】Redis事务功能较弱,不建议过多使用 | Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上(可以使用hashtag功能解决) |
6 | 【建议】Redis集群版本在使用Lua上有特殊要求 | (1).所有key都应该由 KEYS 数组来传递,redis.call/pcall 里面调用的redis命令,key的位置,必须是KEYS array, 否则直接返回error,“-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array” (2).所有key,必须在1个slot上,否则直接返回error, “-ERR eval/evalsha command keys must in same slot” |
7 | 【建议】必要情况下使用monitor命令时,要注意不要长时间使用 |
删除时: hscan+hdel LTRIM sscan+srem zscan+zrem 进行删除
14、删除redis bigkey (bigKey的问题)
下面操作可以使用pipeline加速。redis 4.0已经支持key的异步删除,欢迎使用。
- 1、Hash删除: hscan + hdel
public void delBigHash(String host, int port, String password, String bigHashKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
List<Entry<String, String>> entryList = scanResult.getResult();
if (entryList != null && !entryList.isEmpty()) {
for (Entry<String, String> entry : entryList) {
jedis.hdel(bigHashKey, entry.getKey());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigHashKey);
}
- 2、List删除: ltrim
public void delBigList(String host, int port, String password, String bigListKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
long llen = jedis.llen(bigListKey);
int counter = 0;
int left = 100;
while (counter < llen) {
//每次从左侧截掉100个
jedis.ltrim(bigListKey, left, llen);
counter += left;
}
//最终删除key
jedis.del(bigListKey);
}
- 3、Set删除: sscan + srem
public void delBigSet(String host, int port, String password, String bigSetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
List<String> memberList = scanResult.getResult();
if (memberList != null && !memberList.isEmpty()) {
for (String member : memberList) {
jedis.srem(bigSetKey, member);
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigSetKey);
}
- 4、SortedSet删除: zscan + zrem
public void delBigZset(String host, int port, String password, String bigZsetKey) {
Jedis jedis = new Jedis(host, port);
if (password != null && !"".equals(password)) {
jedis.auth(password);
}
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
List<Tuple> tupleList = scanResult.getResult();
if (tupleList != null && !tupleList.isEmpty()) {
for (Tuple tuple : tupleList) {
jedis.zrem(bigZsetKey, tuple.getElement());
}
}
cursor = scanResult.getStringCursor();
} while (!"0".equals(cursor));
//删除bigkey
jedis.del(bigZsetKey);
}
慢日志的问题
15、Redis的pfadd,pfcount?
todo
16、一个 Redis 实例最多能存放多少的 keys? List、 Set、 Sorted Set 他们最多能存放多少元素? 美团
理论上 Redis 可以处理多达 2^32 个 keys, 并且在实际中进行了测试, 每个实例至少存放
了 2 亿 5 千万的 keys。 我们正在测试一些较大的值。 任何 list、 set、 和 sorted set 都可
以放 2^32 个元素。
- 换句话说, Redis 的存储极限是系统中可用的内存值。
17、Redis使用规范
17.1、接入Redis缓存服务
1)在pom.xml中引入依赖,如下:
<dependency>
<groupId>cn.gov.zcy.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>3.0.5-RELEASE</version>
</dependency>
- 在代码中引入spring bean
@AutoWired
private JedisClientUtil jedisClient;
public void testSet(){
jedisClient.set("testKey1", "valueOfTestKey1");
}
/** 正常应该返回: valueOfTestKey1 */
public String testGet(){
return jedisClient.get("testKey1");
}
说明:
- 由于spring-boot-starter-redis 使用配置已经在公共配置中配置了,所以不需要业务方关心redis配置。本组件利用spring boot autoConfiguration 已经自动注册到spring 所以业务方可直接引入bean,开箱即用。**
- 业务可使用定制配置项groupname.business.redis指定redis集群。
- 集群type支持pool(单机),shared(主从),master(sentinel),cluster(集群)模式。(这四种type的区别是什么?)
17.2、使用规范
使用须知
1)dev & test 及其衍生环境使用自建redis,支持redis 所有命令。
2)staging 预发环境和 production 真线环境使用阿里云Redis集群,对某些命令不支持或受限制,详细请参看阿里云Redis命令 集群实例受限制的Redis命令。
3)除真线环境外,其他各个环境均可以自助连接到redis上,进行key的查询;真线环境redis不对外开放,如需查看、删除、修改,请联系@运维 和 @架构组 进行双人操作。禁止任何个人独立操作真线redis key。
键值设计
诸葛亮说过:“夫君子之行,静以修身,俭以养德,非淡泊无以明志,非宁静无以致远。”历史上成功的人,大多都是经历了一番苦痛与孤独,你们需要努力,更需要坚持,更要耐得住寂寞,要坚信自己心中的信念,加油