Redis:一种高性能的键值存储系统

发表时间: 2023-01-11 23:20

redis学习

Redis现在基本上java项目都在使用,Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。

1、redis与memcache区别

① Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过memcache还可用于缓存其他东西,例片、视频等等

② Redis 不仅仅支持简单的k/v 类型的数据,同时还提供list,set,zset,hash等数据结构的存储。而memcache 只支持简单数据类型,需要客户端自己处理复杂对象。

③ Redis只使用单核,而Memcached可以使用多核。所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。

2、Redis常用的五大数据类型

Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储

① String(字符串): string是redis 最基本的类型,一个key对应一个value

应用场景:

String是最常用的一种数据类型,普通的key/value存储都可以归为此类,value其实不仅是String,也可以是数字:比如存放字典

② Hash(哈希):Redis hash 是一个键值(key=>value)对集合,Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

使用场景:存储、读取、修改对象属性

③ List(列表):Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

使用场景:实现最新消息排行等功能。Lists的另一个应用就是消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。

④ Set(集合):Redis的Set是string类型的无序集合。Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。 set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

应用场景:

l 共同好友、二度好友

l 利用唯一性,可以统计访问网站的所有独立IP

⑤ zset(sorted set:有序集合):Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。

应用场景:

l 带有权重的元素,比如一个游戏的用户得分排行榜

l 比较复杂的数据结构,一般用到的场景不算太多

3、redis 持久化

Redis 的读写都是在内存中,所以它的性能较高,但在内存中的数据会随着服务器的重启而丢失,为了保证数据不丢失,我们需要将内存中的数据存储到磁盘,以便 Redis 重启时能够从磁盘中恢复原有的数据,而整个过程就叫做 Redis 持久化。

Redis被称为是内存数据库,那是因为它会将其所有数据存储在内存里,因此Redis具有强劲的速度性能,但是,也正因为数据存储在内存中,当Redis重启后,所有存储在内存的数据就会丢失。为了使得数据持久化,Redis提供了两种方式:RDB方式和AOF方式。

① RDB方式

RDB方式的持久化是通过快照(snapshotting)完成的,当符合一定条件时,Redis会自动将内存中所有的数据生成一份副本并存储在硬盘中,这个过程被称为“快照”。“快照”,就类似于拍照,摁下快门那一刻,所定格的照片,就称为“快照”。

② AOF方式

通过RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照之后更改的所有数据。为了降低因进程中止导致的数据丢失风险,可以使用AOF方式实现数据持久化。

AOF持久化是以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,文件中可以看到详细的操作记录。

4、redis 分布式锁

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。比如日常开发中,秒杀下单、抢红包等等业务场景,都需要用到分布式锁。而Redis非常适合作为分布式锁使用。



l 「互斥性」: 任意时刻,只有一个客户端能持有锁。

l 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。

l 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。

l 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。

l 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除

redis中的事务并不像mysql中那么完美,只是简单的保证了原子性。redis中提供了四个命令来实现事务,MULTI:类似于mysql中的BEGIN;EXEC:类似于COMMIT;DISCARD类似于ROLLBACK;WATCH则是用于来实现mysql中类似锁的功能。

方案一:SETNX + EXPIRE

使用setnx抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。

假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下:

但是这个方案中,setnx和expire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,「别的线程永远获取不到锁啦」。

方案二:SETNX + value值是(系统时间+过期时间)

为了解决方案一,「发生异常锁得不到释放的场景」,把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。



这个方案的优点是,巧妙移除expire单独设置过期时间的操作,把「过期时间放到setnx的value值」里面来。解决了方案一发生异常,锁得不到释放的问题。但是这个方案还有别的缺点:

l 过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。

l 如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖

l 该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁。


方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)

Redis是高性能的key-value内存数据库,在部分场景下,是对关系数据库的良好补充。
Redis提供了非常丰富的指令集,官网上提供了200多个命令。但是某些特定领域,需要扩充若干指令原子性执行时,仅使用原生命令便无法完成。
Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。

使用方式:

redis在服务器端内置lua解释器(版本2.6以上)

  redis-cli提供了EVAL与EVALSHA命令执行Lua脚本:

  redis内置lua执行命令

  EVAL命令语法

  EVAL script numkeys key [key …] arg [arg …]

  EVAL —lua程序的运行环境上下文

  script —lua脚本

  numkeys —参数的个数(key的个数)

  key —redis键 访问下标从1开始,例如:KEYS[1]

  arg —redis键的附加参数

  EVALSHA 命令语法

  EVALSHA SHA1 numkeys key [key …] arg [arg …]

  EVALSHA命令允许通过脚本的SHA1来执行(节省带宽)

  Redis在执行EVAL/SCRIPT LOAD后会计算脚本SHA1缓存, EVALSHA根据SHA1取出缓存脚本执行.

  Redis中管理Lua脚本

  script load script 将Lua脚本加载到Redis内存中(如果redis重启则会丢失)

  script exists sh1 [sha1 …] 判断sha1脚本是否在内存中

  script flush 清空Redis内存中所有的Lua脚本

  script kill 杀死正在执行的Lua脚本。(如果此时Lua脚本正在执行写操作,那么script kill将不会生效)

  Redis提供了一个lua-time-limit参数,默认5秒,它是Lua脚本的超时时间,如果Lua脚本超时,其他执行正常命令的客户端会收到“Busy Redis is busy running a script”错误,但是不会停止脚本运行,此时可以使用script kill 杀死正在执行的Lua脚本。

  lua函数

  主要有两个函数来执行redis命令

  redis.call() – 出错时返回具体错误信息,并且终止脚本执行

redis.pcall() –出错时返回lua table的包装错误,但不引发错误

使用流程如下:

  1.编写脚本

  2.脚本提交到REDIS并获取SHA

3.使用SHA调用redis脚本

实例:

需求:实现一个访问频率控制,某个IP在短时间内频繁访问页面,需要记录并检测出来,就可以通过Lua脚本高效的实现。



Lua优点:

① 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。

② 原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。

③ 复用。客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

lua缺点:

① 因为 Redis LUA,等于是在C里面调LUA,然后LUA里面再去调 C,返回值会有两次的转换,先从Redis协议返回值转成LUA对象,再由LUA对象转成 C的数据返回。

② 有很多LUA解析,VM处理,包括lua.vm内存占用,会比一般的命令时间慢。建议用LUA最好只写比较简单的,比如if判断。尽量避免循环,尽量避免重的操作,尽量避免大数据访问、获取。因为引擎只有一个线程,当CPU被耗在LUA的时候,只有更少的CPU处理业务命令,所以要慎用。

方案四:SET的扩展命令(SET EX PX NX)

除了使用,使用Lua脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]),它也是原子性的!


伪代码:



方案存在问题:

l 锁过期释放了,业务还没执行完 。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。

l 锁被别的线程误删。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。


方案五:SET EX PX NX + 校验唯一随机值,再释放锁

方案四锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下



缺点:锁过期释放,业务没执行完

方案六: 开源框架~Redisson

开源框架Redisson解决,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

Redission使用流程:



watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

如果时集群的话还有一个问题:就是redis的master节点宕机了,而锁没来得及复制到slave节点

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放;抢到锁后会开辟一个分线程看门狗去续时,最后在finally代码快中删除锁。

缺点:基于单机版实现


方案七:多机实现的分布式锁Redlock

Redis一般都是集群部署的



如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了

为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的:

搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

假设有N个redis的master节点,这些节点是相互独立的(不需要主从或者其他协调的系统)。N推荐为奇数。具体算法如下:



缺点:

Redisson RedLock 是基于联锁 MultiLock 实现的,但是使用过程中需要自己判断 key 落在哪个节点上,对使用者不是很友好。Redisson RedLock 已经被弃用,直接使用普通的加锁即可,会基于 wait 机制将锁同步到从节点,但是也并不能保证一致性。仅仅是最大限度的保证一致性。

5、redis和数据库的一致性问题的解决方案

使用到缓存的场景:

同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

(1)请求 A 进行写操作,删除缓存

(2)请求 B 查询发现缓存不存在

(3)请求 B 去数据库查询得到旧值

(4)请求 B 将旧值写入缓存

(5)请求 A 将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。


采用读取 biglog 异步删除缓存

重试删除缓存机制还可以,就是会造成好多业务代码入侵。其实,还可以通过**数据库的 binlog 来异步淘汰 key **。


以 mysql 为例 可以使用阿里的 canal 将 binlog 日志采集发送到 MQ 队列里面,然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

推荐使用先更新数据库,再删除缓存。只有很低的概率会出现数据不一致的情况,而且使用 redis 就是为了快速,保证强一致性一定会有性能上的损失,只能保证最终一致性。



6、缓存设计

1) 穿透优化

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。 整个过程分为如下3步:

① 缓存层不命中。

② 存储层不命中,不将空结果写回缓存。

③ 返回空结果

解决办法:

Ø 缓存空对象

存储层不命中后,仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。

缓存空对象会有两个问题:

第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。

第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

类似代码实现如下:

2)布隆过滤器拦截

bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,下面先来简单的实现下看看效果,我这里用guava实现的布隆过滤器:

由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。

Ø 击穿

击穿与雪崩的不同在于缓存key失效的量级上。击穿是对于单个key值的缓存失效过期,雪崩则是大面积key同时失效。

解决办法:

l 若缓存数据基本不会发生更新,则可尝试将热点数据设置为永不过期。

l 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。

l 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

Ø 缓存雪崩

由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩。

解决办法:

l 可以把缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。利用sentinel或cluster实现。

l 采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底

l 缓存的过期时间用随机值,尽量让不同的key的过期时间不同(例如:定时任务新建大批量key,设置的过期时间相同)

7、Redis cluster集群介绍

① 常见的几种模式对比:

模式

版本

优点

缺点

主从模式

redis2.8之前

1、解决数据备份问题

2、做到读写分离,提高服务器性能


1、master故障,无法自动故障转移,需人工介入

2、master无法实现动态扩容


哨兵模式

redis2.8级之后的模式

1、Master 状态监测

2、master节点故障,自动切换主从,故障自愈

3、所有slave从节点,随之更改新的master节点


1、slave节点下线,sentinel不会对一进行故障转移,连接从节点的客户端因为无法获取到新的可用从节点

2、master无法实行动态扩容

redis cluster模式

redis3.0版本之后

1、无中心架构。

2、数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。

3、可扩展性,可线性扩展到 1000 个节点(官方推荐不超过 1000 个),节点可动态添加或删除。

4、高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升。

5、降低运维成本,提高系统的扩展性和可用性。


1、Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。

2、节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。

数据通过异步复制,不保证数据的强一致性。

3、多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。

4、Slave 在集群中充当 “冷备”,不能缓解读压力,当然可以通过 SDK 的合理设计来提高 Slave 资源的利用率。



一个 Redis Cluster 由多个 Redis 节点构成,不同节点组服务的数据没有交集,也就是每个一节点组对应数据 sharding 的一个分片。

节点组内部分为主备两类节点,对应 master 和 slave 节点。两者数据准实时一致,通过异步化的主备复制机制来保证。

采用gossip 协议消息:

gossip 协议包含多种消息,包括 ping,pong,meet,fail 等等。

ping:每个节点都会频繁给其他节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据;

pong: 返回 ping 和 meet,包含自己的状态和其他信息,也可以用于信息广播和更新;

fail: 某个节点判断另一个节点 fail 之后,就发送 fail 给其他节点,通知其他节点,指定的节点宕机了。

meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信,不需要发送形成网络的所需的所有 CLUSTER MEET 命令。发送 CLUSTER MEET 消息以便每个节点能够达到其他每个节点只需通过一条已知的节点链就够了。由于在心跳包中会交换 gossip 信息,将会创建节点间缺失的链接。

gossip 的优缺点:

优点: gossip 协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新有一定的延时,降低了压力; 去中心化、可扩展、容错、一致性收敛、简单。 由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

缺点: 元数据更新有延时可能导致集群的一些操作会有一些滞后。 消息的延迟 , 消息冗余 。

数据分布:

Redis Cluster 中,Sharding 采用 slot (槽) 的概念,一共分成 16384 个槽,这有点儿类似 pre sharding 思路。对于每个进入 Redis 的键值对,根据 key 进行散列,分配到这 16384 个 slot 中的某一个中。使用的 hash 算法也比较简单,就是 CRC16 后 16384 取模 [crc16(key)%16384]。

Redis 集群中的每个 node (节点) 负责分摊这 16384 个 slot 中的一部分,也就是说,每个 slot 都对应一个 node 负责处理。

每一个节点负责维护一部分槽及槽所映射的键值数据:


8、Redission 连接 cluster

修改 redisson.yml 文件

注意,nodeAddresses 对应的节点都是 master。

采用 哈希虚拟槽分区 的特性:

l 解耦了数据和节点之间的关系,简化了节点的扩容和收缩的难度

l 节点自身来维护槽的映射关系,不需要客户端或者代理来维护槽分区的元数据

l 至此节点、槽、键之间的映射查询

9、redis安装部署

下载源码包:https://redis.io/download

下载对应的版本

上传离线压缩包或者下载到公共目录:/usr/local/redis/

① 在线wget安装:

② 解压:tar -zxvf redis-6.2.6.tar.gz

③ 一般都会将redis目录放置到 /usr/local/redis目录:mv redis-6.2.6 /usr/local/redis

④ cd /usr/local

⑤ 编译:cd redis

⑥ 安装:make PREFIX=/usr/local/redis install

⑦ 启动:

根据上面的操作已经将redis安装完成了。在目录/usr/local/redis 输入下面命令启动redis

./bin/redis-server ./redis.conf

⑧ 以下修改配置文件允许远程连接:

修改配置文件:**这里我要将daemonize改为yes,同时也将#bind 127.0.0.1注释,将protected-mode设置为no。这样启动后我就可以在外网访问了。

密码配置:使用命令 /requirepass 快速查找到 # requirepass foobared 然后去掉注释,这个foobared改为自己的密码。也可以不加密码。


开机启动配置:

echo "
/usr/local/redis/bin/redis-server /etc/redis/redis.conf &" >> /etc/rc.local

查看Redis是否正在运行,命令如下:



10、Redis数据库连接工具测试
官网下载:
https://redisdesktop.com/download

重载系统服务:

systemctl daemon-reload

测试并加入开机自启: