Redis
文章目录
1. NoSQL 数据库
NoSQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,特别是大数据应用难题。
NoSQL最常见的解释是“non-relational”, “Not Only SQL”也被很多人接受。NoSQL仅仅是一个 概念 ,泛指非关系型的数据库,区别于关系数据库关系数据库,它们 不保证关系数据的ACID特性 。NoSQL是一项全新的数据库革命性运动,其拥护者们提倡运用 非关系型的数据存储 ,相对于铺天盖地的关系型数据库运用,这一概念无疑是一种全新的思维的注入。
NoSQL数据库的分类:
-
键值(Key-Value)存储数据库、
- 这一类数据库主要会使用到一个哈希表,这个表中有一个特定的键和一个指针指向特定的数据。Key/value模型对于IT系统来说的优势在于简单、易部署。但是如果数据库管理员(DBA)只对部分值进行查询或更新的时候,Key/value就显得效率低下了。举例如:Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB。
-
列存储数据库
- 这部分数据库通常是用来应对分布式存储的海量数据。键仍然存在,但是它们的特点是指向了多个列。这些列是由列家族来安排的。如:Cassandra, HBase, Riak.
-
文档型数据库
- 文档型数据库的灵感是来自于Lotus Notes办公软件的,而且它同第一种键值存储相类似。该类型的数据模型是版本化的文档,半结构化的文档以特定的格式存储,比如JSON。文档型数据库可以看作是键值数据库的升级版,允许之间嵌套键值,在处理网页等复杂数据时,文档型数据库比传统键值数据库的查询效率更高。如:CouchDB, MongoDb. 国内也有文档型数据库SequoiaDB,已经开源。
-
图形(Graph)数据库
- 图形结构的数据库同其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上。NoSQL数据库没有标准的查询语言(SQL),因此进行数据库查询需要制定数据模型。许多NoSQL数据库都有REST式的数据接口或者查询API。如:Neo4J, InfoGrid, Infinite Graph。
NoSQL适用场景:
- 对数据高并发的读写。
- 海量数据的读写。
- 对数据高可扩展性的。
NoSQL不适用场景:
- 需要事务支持。
- 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。
- 用不到SQL或者使用SQL也无法解决问题,可以考虑使用NoSQL。
2. Redis 概述
- Redis是一个开源的key-value存储系统。
- 与Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。
- 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
- 在此基础上,Redis支持各种不同方式的排序。
- 与memcached一样,为了保证效率,数据都是缓存在内存中。
- 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。
- 并且在此基础上实现了master-slave(主从)同步。
Redis的应用场景:
配合关系型数据库做高速缓存
3. Redis的数据类型
4. Redis 配置文件
5. Redis 发布与订阅
-
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
-
Redis 客户端可以订阅任意数量的频道。
发布与订阅示意图:
- 发布:
- 订阅
命令行实现:
- 打开一个客户端订阅channel1
SUBSCRIBE channel1
- 打开另一个客户端,给channel1发布消息 hello
publish channel1 hello
- 打开第一个客户端可以看到发送的消息
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello"
6. Jedis
7. Redis事务与锁机制
8. Redis持久化操作
9. 主从复制
9.1 简介
主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master 以写为主,Slave 以读为主。从而提升系统的高可用性。
主从复制带来的好处:
- 读写分离,性能扩展
- 容灾快速恢复
9.2 搭建一主多重 (Docker)
首先用Docker拉取Redis的镜像:
docker pull redis
查看拉取的镜像:
[root@ls_pne3xf1w ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 3edbb69f9a49 3 days ago 117MB
启动3个Redis容器(一主,二从):
- 主机:
docker run -itd --name redis-master -p 6379:6379 redis:latest
- 从机①:
docker run -itd --name redis-slave-1 -p 6380:6380 redis:latest
- 从机②:
docker run -itd --name redis-slave-2 -p 6381:6381 redis:latest
查看启动的容器:
docker ps
三个容器的名称分别是:redis-master、redis-slave-1、redis-slave-2
查看用于做主机的redis-master容器的信息:
docker inspect redis-master
主机容器的IP地址是:172.17.0.2
进入从机redis-slave-1的Redis命令行中:
docker exec -it redis-slave-1 redis-cli
将从机redis-slave-1的主机设置为redis-master
:
SLAVEOF 172.17.0.2 6379
redis-slave-2也做相同操作。
使用info replication
命令可以查看主从配置信息:
info replication
这里查看redis-master,发现已经有两个从机了:
10. 哨兵模式
10.1 概述
-
sentinel 哨兵主要用来监控redis主从集群,提高了redis 主从集群的可用性。
-
哨兵机制解决了主从复制架构中的主节点宕掉后从节点无法作为主节点对外服务的问题
-
Sentinel(哨兵)是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例 组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。简单的说哨兵就是带有自动故障转移功能的主从架构。
10.2 哨兵模式实现机制
哨兵可以设置多个,哨兵刚开始监控主节点,向主节点发送心跳检测,检测主节点是否还在运行,一旦半数以上的哨兵判断主节点已经挂掉了 ,那么就会从从节点中选举出一个节点作为主节点对外服务。
10.3 配置哨兵模式
注意:哨兵模式基于主从复制,也就是说需要一台主机搭配多台从机实现。
实际搭建可参考文章:docker 配置redis主从,哨兵sentinel、Redis使用详解
10.4 故障恢复
哨兵对Redis的主从服务进行监控,如果主机挂掉了会选举新的主机,保证整个系统的高可用。
选举新节点的规则:
- 根据优先级别:
slave-priority
(新版本属性名称可能已经发生变化)- 宕掉的主机在恢复后会变成从机。
按下列三项顺序判断选举那个节点作为主机:
- 优先级在 redis.conf 中默认:
slave-priority
100,值越小优先级越高, - 偏移量是指获得原主机数据最全的 (选数据最全的)
- 每个redis实例启动后都会随机生成一个40位的
runid
,(选runid小的)
10.5 存在的问题
虽然哨兵解决了主从复制架构中的主节点挂掉后从节点无法对外服务的缺陷,但是仍然存在着整个结构只有一台服务进行工作的问题,在高并发情况下显然是会崩掉的,而且由于做持久化,久而久之AOF文件会越来越大,服务器迟早会崩掉,所有我们要做成集群的模式,多台Redis对外进行服务,实现高可用。
11. 集群 (Redis Cluster无中心结构集群模式)
11.1 概述
- Redis的集群与其他的集群有所不同,Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
- Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
- 同时Redis的集群不需要代理,也就是不需要向Tomcat服务器集群一样需要Nginx这样的反向代理服务器进行代理负载均衡,连接Redis的任何一个节点都可以连接到这个集群。
11.2 搭建集群环境
参考文章:
11.3 slots 插槽&CRC16算法
Redis的集群中的节点存储的数据时不同的,就是数据的分片,如何分片呢?,如何进行数据的寻找呢?
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个, 集群使用公式 CRC16(key) % 16384
来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。也就是说我们操作Redis放置一个key,会先计算属于那个槽,从而判断出这个key要放到那个Redis节点中。不同的key通过CRC16计算肯定是不同的,但是对16383求余之后可能得到相同的值,即不同的key占用的槽位是相同的,但是不影响,直接存储即可。
集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
- 节点 A 负责处理 0 号至 5460 号插槽。
- 节点 B 负责处理 5461 号至 10922 号插槽。
- 节点 C 负责处理 10923 号至 16383 号插槽。
11.4 故障恢复
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?
- 如果某一段插槽的主从都挂掉,而
cluster-require-full-coverage
为 yes (默认),那么 ,整个集群都挂掉。 - 如果某一段插槽的主从都挂掉,而
cluster-require-full-coverage
为 no ,那么,该插槽数据全都不能使用,也无法存储,但是集群不会挂。
11.5 总结
Redis集群与一般的集群不同,它不需要提供代理服务就可以正常访问,正常工作。
Redis集群的好处:
- 实现扩容
- 分摊压力
- 无中心配置相对简单
不足之处:
- 多键操作是不被支持的(无法一次设置多组key-value)
- 多键的Redis事务是不被支持的。lua脚本不被支持
- 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。
12. 三大缓存问题
12.1 缓存穿透
问题描述:
缓存穿透 是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
key
对应的数据在数据源并不存在,每次针对此 key
的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的 用户id
获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
简单来讲:
- 查询不存在的数据导致Redis的命中率下降,压力来到数据库。
- 要查询的key在数据库中也同样查询不到,导致数据库中并发的去执行了很多不必要的查询操作,从而导致巨大冲击和压力,最终可以能导致数据库崩溃。
解决方案:
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
所以有这样几个解决方案:
- 对空值缓存: 如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。
- 设置可访问的名单(白名单): 使用
bitmaps
类型定义一个可以访问的名单,名单id作为bitmaps
的偏移量,每次访问和bitmap
里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。 - 添加过滤器,如布隆过滤器 :(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难)。 将所有可能存在的数据哈希到一个足够大的
bitmaps
中,一个一定不存在的数据会被 这个bitmaps
拦截掉,从而避免了对底层存储系统的查询压力。 - 进行实时监控: 当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
12.2 缓存击穿
问题描述:
缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。
解决方案:
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
- 预先设置热门数据: 在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长。
- 实时调整: 现场监控哪些数据热门,实时调整key的过期时长。
- 使用锁: (一般都是分布式锁 redisson等) 大量请求进来如果缓存失效,那么加锁只有一个线程去查询数据库,查到数据后将结果放入缓存,后面的线程直接从缓存中获取即可。
12.3 缓存雪崩
问题描述:
缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
解决方案:
- **构建多级缓存架构:**nginx缓存 + redis缓存 +其他缓存(ehcache等)
- 使用锁或队列: 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。但不适用高并发情况。
- 设置过期标志更新缓存: 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
- 将缓存失效时间分散开: 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
13. 分布式锁
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种 跨JVM的互斥机制 来控制共享资源的访问,这就是分布式锁要解决的问题。
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等)
- 基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
- 性能:redis更高
- 可靠性:zookeeper更高
Redis实现分布式锁是利用Redis内置的setnx
命令实现的。
注意: 设置的锁是一个键值对,key应该一致。
加锁:
setnx 锁名称 值
setnx lock xxx
解锁:
del lock
设置锁的过期时间:
EXPIRE(key,seconds)
大致逻辑是这样的:
一个线程先开始执行业务,在Redis中使用setnx
命令设置锁,其它线程在处理业务之前先判断是否已经设置了锁,如果已经设置了锁就进入阻塞状态。
但是会出现一些问题:
-
线程A执行业务需要的时间非常长,其它线程就会进入阻塞状态
- 可以通过为锁设置过期时间的方式解决。
-
线程A执行过程中,锁自动过期了,此时恰好线程B获取到了锁并且正在处理业务,线程A开始释放锁,结果导致线程B的锁被释放。
- 可以通过设置UUID的方式解决。设置锁的时候先生成一个UUID,这个UUID就代表了当前线程加的锁,执行完业务后释放锁时,先判断设置到Redis中的锁的value值是否与该线程创建的UUID一致,如果一直就释放锁;不一致就什么都不做。
-
锁的释放也就是删除操作缺乏原子性,线程A在判断完UUID发现是自己的锁,于是开始释放锁操作,但是这时锁刚好自动过期了,并且被线程B获取到,那么最后线程A释放的锁就是线程B的锁。
-
可以通过LUA脚本保证删除锁的原子性,把判断UUID的值与删除操作绑定到一起。
-
官网上给出的LUA脚本示例:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
-
更多分布式锁相关内容可以参考: