本笔记主要涉及学习 redis 过程中做的一些笔记,包含最初比较浅显的了解(所以会有一些比较简单的笔记描述),到后面对其应用的逐渐学习和补齐。其中主要参考书籍为 《redis in action Josiah L. Carlson》其他参考博客也会注明。
Redis (Remote Dictionary Server) is an open-source in-memory data structure project implementing a distributed, in-memory key-value database with optional durability.
Redis 作为一种远程缓存服务,可以帮助DB抗一些请求来做高性能的数据查询
完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销。
数据结构简单,对数据操作也简单。Redis 中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis 正是依赖这些灵活的数据结构,来提升读取和写入的性能。
采用单线程,省去了很多上下文切换的时间以及 CPU 消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。
使用基于 IO 多路复用机制的线程模型,可以处理并发的链接。
数据类型 | 底层数据结构 | 应用 | 备注 |
---|---|---|---|
string | simple dynamic string or long(数字) | 粉丝数、关注的人数 | |
list | ziplist or linkedlist(双向无环链表) | 关注列表、粉丝列表 | |
hash | ziplist or dict | 存储对象 | |
set | intset(都是数字) or dict | 共同喜好、各自的喜好等 | |
sorted set | ziplist or skiplist+dict | 最近访问的服务、排行榜等 |
Redis 数据类型
List: 按照插入的顺序排序的字符串链表。和数据结构中的普通链表一样,可以在其头部(left)和尾部(right)添加新的元素。在插入元素时,如果该键不存在,Redis将为该键创建一个新的链表。如果链表中所有的元素均被移除,那么该键也会从数据库中删除。
redis整个是一个单线程服务。启动后即陷入巨大的while循环,不停地处理文件事件和时间事件。
整个流程是这样的:
beforeSleep -> epollwait -> 处理请求 -> 定时事件 ->xx
建议: 减少大 key,减少耗时命令。
beforeSleep的执行频率一般比定时事件更频繁一些。主要做以下几件事:
redis是一个内存服务,会设定内容上限的。
Redis 之 LRU 与 LFU
逐出 - 当执行write但内存达到上限时,强制将一些key删除
特点:
建议:关注逐出qps,过高会影响正常请求处理
过期 - 当某个key到达了ttl(time to live)时间,认为该key已经失效
两种方式:
惰性删除 - 读、写操作前判断ttl,如过期则删除
定期删除 - 在redis定时事件中随机抽取部分key判断ttl
特点
并不一定是按设置时间准时地过期
定期删除的时候会判断过期比例,达到阈值才退出
建议:打散key的过期时间,避免大量key在同一时间点过期
redis虽然作为一个缓存存在,通常作为业务和DB之间的一个衔接,如果只保存在内存中,不进行持久化机器宕机之后就会造成数据的丢失。
ps: 内存和磁盘有什么样的区别??
持久化 - 将内存中的数据dump到磁盘文件
RDB持久化(一次性写入)
AOF持久化(总是在写入追加,因此这个文件会越来越大,因此也就有了AOF重写)
应用:利用AOF文件灾备
主从模式
全量同步
部分同步
优点:
优点:
缺点:
建议:利用pipeline代替mget,且控制一次请求的命令数量(建议50以内)(因为proxy做分发也会产生一定的压力)
注:
和pipeline的区别
一致性hash:
redis cluster(涉及缓存的业务尽量用这种集群方式):
什么是服务发现
服务发现即在微服务场景下,将容器应用部署到集群时,其服务地址是由集群系统动态分配的。那么,当我们需要访问这个服务时,如何确定它的地址呢?这时就需要服务发现(Service Discovery)有客户端发现和服务端发现(Kubernetes 和 Marathon 这样的部署环境会在每个集群上运行一个代理,将代理用作服务端发现的负载均衡器。客户端使用主机 IP 地址和分配的端口通过代理将请求路由出去,向服务发送请求。代理将请求透明地转发到集群中可用的服务实例。)
references:
Redis的分布式锁详解
如何部署一个生产级别的 Kubernetes 应用
Redis setnx 原子操作:
最近做业务用到了消息队列,一般 mq 会保证 at least once, 但随之而来的问题就是有可能出现重复消费的现象,业务方需要做消费幂等。此时可以利用 redis 来做消费幂等。
references:
问题背景:
原子性:如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,即要么一起成功要么一起失败。将整个操作视作一个整体是原子性的核心特征。
由于 Redis 是单线程的,因此每一个指令都是原子性的,但这不意味着 redis 的事务就是原子性的,其不会回滚,即只保证了一致性和隔离性,不能保证原子性和持久性。
我在一开始使用时用 exists 判断 key 是否存在,然后 set 值,这样不能保证原子性,
但还好 redis 提供了 setnx 操作,可以检测不存在时再存入,同时可以用 set 指令添加参数。否则返回 0,把说明其他进程已经获得了锁,通过这种方式则可以实现「锁」的机制。
基于 setnx 实现方式:
总结为以下几点:
del key
缺点:
基于多个 Redis 集群部署的高可用分布式锁解决方案:RedLock:
redis 官方给出了基于多个 redis 集群部署的高可用分布式锁解决方案:RedLock
应用场景:
乐观锁
概念:很乐观,认为什么时候都不会出问题,所以只在更新数据的时候判断一下当前数据有没有被别人变更过。
redis 相关:redis watch 命令+ multi exec 事务就是一个乐观锁,如果 watch 到数据没有变化就执行事务,否则就直接返回错误。
适用场景:写比较少,也就是冲突比较少的情况
悲观锁
概念:很悲观,认为什么时候都会出问题,所以无论什么时候都要加锁(在写之前加锁,写数据的时候其他线程就不会对数据进行修改)。传统的关系型数据库中就用到了比较多悲观锁机制,比如行锁、表锁等
适用场景:写入操作比较频繁的场景。
这是 redis 实现发布与订阅的最简单的方式,但是也有其局限性:
可以参考 《redis in action》pull messaging
使用这种方式能够实现消息的持久化,书中也提到了实现多播的方式
Redis(8)——发布/订阅与Stream
Redis入门 - 数据类型:Stream详解
记一次redis stream数据类型内存不释放问题
Redis Stream类型的使用
把Redis当作队列来用,真的合适吗?
Using Redis Stream with Python
# 追加消息
xadd key_name * field_name 'value'
# 从第一条开始消费
xgroup create key_name consumer_group 0-0
xreadgroup group consumer_group consumer1 COUNT 1 STREAMS key_name 0-0
# 从上条被消费的消息可以理解成从 last-delivered-id 开始消费
xreadgroup group consumer_group consumer_1 COUNT 1 STREAMS key_name >
# ack
xack cloud_resource consumer_group 1553585533795-0
# 查看消息列表
xrange key_name - +
# 修剪 stream 长度
xtrim cloud_resource MAXLEN 10
# 另一种是直接在 xadd 中定义即可
xadd key_name maxlen ~ 1 * field_name 'value'
# 其他操作可以参考文章中介绍
注:
关于内存占用:
xreadgroup group consumer_group consumer1 COUNT 1 STREAMS key_name 0-0
能读到,但没在业务中使用,因为可能造成重复消费。关于重复消费: