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区别?(比较学习法)
  1. rdbms:组织结构化数据;结构化查询语言;数据和关系都存储在单独的表中;严格的一致性
  • 关系型数据库的缺点
  • 1、关系数据库存储的是行记录,无法存储数据结构
  • 以微博的关注关系为例,“我关注的人”是一个用户 ID 列表,使用关系数据库存储只能将列表拆成多行,然后再查询出来组装,无法直接存储一个列表。
  • 2、关系数据库的 schema 扩展很不方便
  • schema 是强约束,操作不存在的列会报错,业务变化时扩充列也比较麻烦,需要执行 DDL(data definition language,如 CREATE、ALTER、DROP 等)语句修改,而且修改时可能会长时间锁表
  • 例如,MySQL 在对大批量数据进行操作时,可能锁表 1 个小时
  • 3、关系数据库在大数据场景下 I/O 较高
  • 如果对一些大量数据的表进行统计之类的运算,关系数据库的 I/O 会很高,因为即使只针对其中某一列进行运算,关系数据库也会将整行数据从存储设备读入内存中。
  • 4、关系数据库的全文搜索功能比较弱
  • 关系数据库的全文搜索只能使用 like 进行整表扫描匹配,性能非常低,在各种复杂的场景下无法满足业务要求。
  • 例如:对品牌表中适用类目的的全文检索,导致慢查询性能问题
  • 再如:模糊查询商品名称,会导致慢查询性能问题
  1. 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过期后得到什么值 redis过期数据的处理_redis

我们在项目中用到的:首页的访问量是整个系统中最大的,所以需要使用缓存来减轻读取数据库的压力。对于首页商品的广告位展示, 使用了 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过期后得到什么值 redis过期数据的处理_缓存的过期策略_02

  • 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-entrieszset-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)先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加的会被放在表尾方向。

redis过期后得到什么值 redis过期数据的处理_数据类型_03

  • 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、消息队列
  • 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>
  1. 在代码中引入spring bean
@AutoWired
private JedisClientUtil jedisClient;

public void testSet(){
    jedisClient.set("testKey1", "valueOfTestKey1");
}

/** 正常应该返回: valueOfTestKey1 */
public String testGet(){
    return jedisClient.get("testKey1");
}

说明:

  1. 由于spring-boot-starter-redis 使用配置已经在公共配置中配置了,所以不需要业务方关心redis配置。本组件利用spring boot autoConfiguration 已经自动注册到spring 所以业务方可直接引入bean,开箱即用。**
  2. 业务可使用定制配置项groupname.business.redis指定redis集群。
  3. 集群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。
键值设计

诸葛亮说过:“夫君子之行,静以修身,俭以养德,非淡泊无以明志,非宁静无以致远。”历史上成功的人,大多都是经历了一番苦痛与孤独,你们需要努力,更需要坚持,更要耐得住寂寞,要坚信自己心中的信念,加油