Redis简介
Redis 是一个基于内存的高性能 key-value 数据库。是完全开源免费的,用C语言编写的,遵守BSD协议
Redis 特点
Redis 是基于内存操作的,吞吐量非常高,可以在 1s内完成十万次读写操作
Redis 的读写模块是单线程,每个操作都具原子性
Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启可以再次加载,但可能会有极短时间内数据丢失
Redis 支持多种数据结构
Redis优势
1、高性能
Redis 是基于内存的存储系统,因此它的读写速度非常快,通常可以在微秒级别响应请求。
2、丰富的数据结构
Redis 支持多种数据结构,使得它可以处理不同类型的复杂数据,而不仅仅是简单的键值对。
3、灵活的持久化
Redis 提供了多种持久化选项,用户可以根据需求选择数据如何持久化,甚至可以禁用持久化以获得最佳性能。
4、分布式和高可用性
Redis 提供了复制、哨兵模式(Sentinel)和 Redis 集群等功能,可以构建分布式的高可用系统。
5、轻松扩展
Redis 支持分片(sharding),使得数据可以分布在多个节点上,从而轻松扩展系统容量和性能。
Redis 的应用场景
1、缓存
Redis 最常见的应用场景之一是作为缓存系统,它可以极大地提高数据访问速度,减轻数据库负载。由于 Redis 的高性能,它非常适合用于存储频繁访问的数据,如会话数据、热门商品信息等。
2、排行榜和计数器
Redis 的有序集合使得实现排行榜功能变得非常简单,而其高性能则可以实时更新和获取数据。此外,Redis 的原子递增和递减操作非常适合用于计数器,如网站的访问量统计。
3、实时分析
Redis 的位图、HyperLogLog 和流等数据类型非常适合用于实时分析数据,如在线用户数统计、日志处理和事件追踪。
4、消息队列
Redis 的列表结构可以用来实现简单的消息队列,而发布/订阅功能则可以实现复杂的实时消息系统,如聊天应用和实时通知系统。
5、分布式锁
Redis 的原子操作使得它可以用于实现分布式锁,从而在分布式系统中控制对共享资源的访问。
6、会话存储
由于 Redis 的高性能和丰富的数据结构,它被广泛用于存储用户会话信息,特别是在需要快速读取和写入的情况下。
数据持久化
虽然 Redis 主要将数据存储在内存中,但它也支持持久化功能,允许将数据异步地保存到磁盘中。
Redis 提供了两种主要的持久化方式:
RDB(Redis Database File):在指定的时间间隔内生成数据的快照并将其保存到磁盘。
AOF(Append Only File):将每个写操作日志记录下来,并在 Redis 启动时重新执行这些操作以恢复数据。
如果你追求性能,同时仍然可以承受数分钟内的数据丢失的话,那么可以使用 RDB 持久化。
如果你非常关心你的数据,并且性能对性能要求不是那么高的话,那么可以使用 AOF 持久化。
注:Redis 支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。
事务(Transactions)
Redis 支持简单的事务,允许在一个操作序列中执行多个命令,并且保证这些命令按顺序执行。事务使用 MULTI、EXEC、DISCARD 和 WATCH 命令实现。
发布/订阅(Pub/Sub)
Redis 支持消息发布/订阅模式,使得消息可以在不同的 Redis 客户端之间实时传递。这对于构建实时通知系统、聊天应用和数据流处理系统非常有用。
Lua 脚本
Redis 支持通过 Lua 脚本执行原子操作,可以将多个命令组合成一个脚本来避免多次网络通信和数据竞争。
Redis的数据类型:
1、5中基本类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
2、3中特殊类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
3、命令介绍
String命令:
String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
应用场景
需要存储常规数据的场景
举例:缓存 Session、Token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。
相关命令:SET、GET。
需要计数的场景
举例:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
相关命令:SET、GET、 INCR、DECR 。
分布式锁
利用 SETNX key value 命令可以实现一个最简易的分布式锁(存在一些缺陷,通常不建议这样实现分布式锁,可以使用redission)。
List命令:
Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
理解 RPUSH , LPOP , lpush , RPOP 命令:
应用场景
信息流展示
举例:最新文章、最新动态。
相关命令:LPUSH、LRANGE。
消息队列
List 可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
相对来说,Redis 5.0 新增加的一个数据结构 Stream 更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。
Hash命令:
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。
Hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 Hash 做了更多优化。
应用场景
对象数据存储场景
举例:用户信息、商品信息、文章信息、购物车信息。
相关命令:HSET (设置单个字段的值)、HMSET(设置多个字段的值)、HGET(获取单个字段的值)、HMGET(获取多个字段的值)。
Set命令:
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,类似于 Java 中的 HashSet 。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。
你可以基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
应用场景
需要存放的数据不能重复的场景
举例:网站 UV 统计(数据量巨大的场景还是 HyperLogLog更适合一些)、文章点赞、动态点赞等场景。
相关命令:SCARD(获取集合数量) 。
需要获取多个数据源交集、并集和差集的场景
举例:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。
相关命令:SINTER(交集)、SINTERSTORE (交集)、SUNION (并集)、SUNIONSTORE(并集)、SDIFF(差集)、SDIFFSTORE (差集)。
需要随机获取数据源中的元素的场景
举例:抽奖系统、随机点名等场景。
相关命令:SPOP(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER(随机获取集合中的元素,适合允许重复中奖的场景)。
Sorted Set命令
Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。
应用场景
需要随机获取数据源中的元素根据某个权重进行排序的场景
举例:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
相关命令:ZRANGE (从小到大排序)、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。
Redis的客户端操作有以下几种方式:
1、jedis
官方推荐的客户端,线程不安全的,底层是由socket实现,是同步的,所以一般需要自己实现连接池使用为佳,可以使用apache的jedispool。
相对来说更加的原生,只支持基本的数据类型如:
String、Hash、List、Set、Sorted Set。
2、redisson
底层是netty实现,是异步非阻塞,线程安全的,相对于jedis来说,redisson对结果有更多的包装,更加抽象,让开发者只关注于业务。
对分布式的支持是它的一大亮点。
支持的数据结构有:
List, Set, Map, Queue, SortedSet, ConcureentMap, Lock, AtomicLong, CountDownLatch
3、lettuce
底层netty实现,也是异步非阻塞的,线程安全的。
4、Redistemplate
是对jedis和lettuce的封装,springboot2.0之后,默认使用 lettuce,使用时只要配置好属性,就能自动由SpringBoot自动管理连接池。
5、总结:
jedis性能较强,jedis的性能至少是RedisTemplate的3倍以上,jedis结合jedisPool使用既可以有高性能又可以保证redis的连接可控。
在性能要求、并发操作不高的场景建议使用RedisTemplate,在并发高,性能要求高的场景下建议使用jedis。
小知识:缓存选型(Redis 和 Memcached 的区别)
1、存储方式上:Memcache 会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部分数据存在硬盘上,这样能保证数据的持久性。
2、数据支持类型上:Memcache 对数据类型的支持简单,只支持简单的 key-value,,而 Redis 支持五种数据类型。
3、使用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、Value 的大小:Redis 可以达到 1GB,而 Memcache 只有 1MB
Redis 缓存的淘汰策略
1、 noeviction(默认策略)
描述:当Redis内存不足时,不执行任何淘汰操作,所有的写操作都会返回错误。这种策略可以确保Redis内存不会被其他进程抢占,但会导致Redis进程被强制杀死,数据全部丢失,因此不建议在生产环境中使用。
适用场景:通常不推荐使用,除非对数据的完整性有极高的要求,且能够接受在内存不足时拒绝所有写操作的后果。
2、 allkeys-lru
描述:从所有key中使用LRU(最近最少使用)算法进行淘汰。LRU算法通过记录每个key的最近访问时间,淘汰最长时间未被访问的key。
适用场景:适用于缓存场景,可以确保经常被访问的数据保留在内存中,提高缓存命中率。
3、volatile-lru
描述:从设置了过期时间的key中使用LRU算法进行淘汰。这种策略只针对设置了过期时间的key进行操作,优先淘汰那些最近最少使用且已经设置了过期时间的key。
适用场景:适用于需要设置过期时间,同时希望缓存尽可能保留热门数据的场景。
4、 allkeys-random
描述:从所有key中随机淘汰数据。这种策略不考虑key的访问频率或过期时间,完全随机选择key进行淘汰。
适用场景:在不确定哪些key是热门数据,或者对淘汰策略没有特殊要求的情况下,可以使用这种简单的随机淘汰策略。
5、volatile-random
描述:从设置了过期时间的key中随机淘汰。与allkeys-random类似,但这种策略只针对设置了过期时间的key进行操作。
适用场景:在需要淘汰过期key,但又不希望完全依赖LRU算法的情况下,可以使用这种随机淘汰策略。
6、volatile-ttl
描述:在设置了过期时间的key中,淘汰过期时间剩余最短的。这种策略优先淘汰那些即将过期的key,确保Redis存储的数据尽可能新鲜。
适用场景:适用于需要快速淘汰即将过期数据的场景,比如缓存即将失效的会话信息等。
7、通过配置文件设置
在Redis的配置文件redis.conf中,可以通过maxmemory-policy属性来设置淘汰策略。例如,要设置淘汰策略为volatile-lru:
maxmemory-policy volatile-lru
同时,还需要通过maxmemory属性来设置Redis能使用的最大内存大小。例如,设置最大内存为100MB:
maxmemory 100mb
或者通过命令动态设置,例如,要设置淘汰策略为allkeys-lru:
CONFIG SET maxmemory-policy allkeys-lru
同样,也可以使用CONFIG SET命令来动态设置最大内存大小。
常用命令
KEYS
KEYS name,返回所有包含 “name” 的 key,即 lastname 和 firstname。
KEYS a??,返回所有以字母 “a” 开头并且后面有两个字符的 key,即 age。
KEYS *,返回所有存在的 key,即 lastname、age 和 firstname。
DEL
DEL key1 key2。若这两个 key 存在且被成功删除,返回的结果是 (integer) 2。
DEL key,若没有成功删除key,那么返回的结果将是 (integer) 0,。
EXISTS
使用 SET 命令分别设置了 key1 和 key2 的值。然后使用 EXISTS 命令来检查指定的 key 是否存在。
EXISTS key1,返回结果为 (integer) 1,表示 key1 存在。
EXISTS key2,返回结果为 (integer) 0,表示 key2不存在。
EXISTS key1 key2 key3,返回结果为 (integer) 2,表示其中两个 key 存在。
EXPIRE
EXPIRE mykey 60 设置键 mykey 在 60 秒后过期
EXPIRE mykey 120 XX 仅当键 mykey 存在时,设置它在 120 秒后过期
EXPIRE mykey 300 NX 仅当键 mykey 不存在时,设置它在 300 秒后过期
EXPIRE mykey 180 GT 仅当键 mykey 的当前过期时间大于 180 秒时,设置它在 180 秒后过期
EXPIRE mykey 240 LT 仅当键 mykey 的当前过期时间小于 240 秒时,设置它在 240 秒后过期
EXPIRE key seconds: 设置键在指定秒数后过期。
PEXPIRE key milliseconds: 设置键在指定毫秒数后过期。
EXPIREAT key timestamp: 设置键在指定的时间戳 (单位为秒) 后过期。
PEXPIREAT key milliseconds-timestamp: 设置键在指定的时间戳 (单位为毫秒) 后过期。
TTL
TTL mykey 若键不存在或者键没有设置过期时间,返回 -1;若键存在并且没有剩余生存时间,返回 0 ,即键已经过期。
TTL 返回的时间单位是秒。
PTTL mykey 若键不存在或者键没有设置过期时间,返回 -1;若键存在并且没有剩余生存时间,返回 0 ,即键已经过期。
PTTL 返回的时间单位是毫秒
TYPE
TYPE key 返回键存储值的类型,可能的返回值有:
“none”:键不存在,“string”:字符串类型,“list”:列表类型,“set”:集合类型,“zset”:有序集合类型,“hash”:哈希类型,“stream”:流类型。
PERSIST
PERSIST mykey 若成功移除过期时间,返回 1;若键不存在过期时间或键不存在,返回 0
Redis 缓存穿透、缓存击穿、缓存雪崩
缓存穿透
查询一个数据库一定不存在的数据。
一般使用缓存流程:数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。
灾难现场:假如有人恶意攻击,就是查询一个不存在的key,利用这个漏洞,对数据库造成压力,甚至压垮数据库。
解决方案:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。
缓存雪崩
是指在某一个时间段,缓存集中过期失效。此刻无数的请求直接绕开缓存,直接请求数据库。
灾难现场:比如天猫双12,会迎来一波抢购,这批商品在23点集中的放入了缓存,假设缓存一个小时,那么到了凌晨24点的时候,这批商品的缓存就都过期了。而这批商品的查询都落到了数据库上,对数据库造成压力,甚至压垮数据库。
解决方案:在设置数据缓存有效期时,在时间后加上一个随机因子;分散缓存过期时间,将热门类数据缓存时间长一点,冷门类的短一点。设置热点数据永不过期。
缓存击穿
是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
灾难现场:比如某个爆款商品(这种爆款很难对数据库服务器造成压垮性的压力。达到这个级别的公司没有几家的。)但我们也要做好防护方案。
解决方案:对爆款商品都是早早的做好了准备,让缓存永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期。
Redis分布式锁
在一个分布式的系统中,经常会涉及到多个节点访问同一个公共资源的情况,此时就需要通过锁来做互斥控制, 避免出现类似于 “线程安全” 的问题。Redis(ZooKeeper)就可以解决这个麻烦,本质上就是使用一个公共的服务器,来记录加锁状态。
使用Redisson框架就可以实现。
主从redis架构中分布式锁存在的问题:
1、线程A从主redis中请求一个分布式锁,获取锁成功;
2、从redis准备从主redis同步锁相关信息时,主redis突然发生宕机,锁丢失了;
3、触发从redis升级为新的主redis;
4、线程B从继任主redis的从redis上申请一个分布式锁,此时也能获取锁成功;
5、导致,同一个分布式锁,被两个客户端同时获取,没有保证独占使用特性;
为了解决这个问题,redis引入了红锁的概念:
需要准备多台redis实例,这些redis实例指的是完全互相独立的Redis节点,这些节点之间既没有主从,也没有集群关系。客户端申请分布式锁的时候,需要向所有的redis实例发出申请,只有超过半数的redis实例报告获取锁成功,才能算真正获取到锁。
Redisson的RedissonRedLock对象实现了Redlock。
红锁其实也并不能解决根本问题,只是降低问题发生的概率。完全相互独立的redis,每一台至少也要保证高可用,还是会有主从节点。既然有主从节点,在持续的高并发下,master还是可能会宕机,从节点可能还没来得及同步锁的数据。很有可能多个主节点也发生这样的情况。
其实,在实际场景中,红锁是很少使用的。这是因为使用了红锁后会影响高并发环境下的性能,使得程序的体验更差。所以,在实际场景中,我们一般都是要保证Redis集群的可靠性。同时,使用红锁后,当加锁成功的RLock个数不超过总数的一半时,会返回加锁失败,即使在业务层面任务加锁成功了,但是红锁也会返回加锁失败的结果。另外,使用红锁时,需要提供多套Redis的主从部署架构,同时,这多套Redis主从架构中的Master节点必须都是独立的,相互之间没有任何数据交互。