Redis笔记搬迁

 
 
Redis(REmote DIctionary Server)
多路复用IO,事件循环(Event Loop)的机制
为什么Redis是单线程执行却能同时处理多个请求?(当然严格来说Redis运行起来并非只有一个线程,但除了主线程之外,Redis的其它线程只是起辅助作用,它们是一些在后台运行做异步耗时任务的线程)
 
单进程单线程,性能高,同时,Redis所有操作都是原子性的,也支持对几个操作合并后原子性的执行。
另外,Redis有丰富的扩展特性,它支持publish/subscribe,通知,key过期等等特性。
 
与其他key-value缓存框架相比:
1⃣️Redis不仅仅支持key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储;
2⃣️Redis支持数据持久化,可以将内存中的数据保存到磁盘,重启的时候可以再加载使用;
3⃣️Redis支持数据的备份,即master-slave模式的数据备份。
 
Redis 快速的原因:
绝大部分请求是纯粹的内存操作(非常快速)
采用单线程,避免了不必要的上下文切换和竞争条件
非阻塞 IO
内部实现采用 epoll,采用了 epoll+自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 io 上浪费一点时间。
 
Redis 为单进程单线程模式,采用队列模式将并发访问变为串行访问。
Redis 本身没有锁的概念,Redis 对于多个客户端连接并不存在竞争,但是在业务客户端对 Redis 进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成
 
 
哪些数据需要缓存?
库存查询 操作记录 品牌数据 看版数据
 
Redis原理
初始化流程和事件循环概述
Redis源码的main函数在源文件server.c中。main函数开始执行后的逻辑可以分为两个阶段:
  • 各种初始化(包括事件循环的初始化);
  • 执行事件循环。
这两个执行阶段可以用下面的流程图来表达(点击看大图):
 
 
从不同的角度来详细介绍redis
网络模型:Redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select,对于单纯只有IO操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个IO调度都是被阻塞住的。
内存管理:Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片,Redis跟据存储命令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临时数据(但会尝试剔除部分临时数据),这点上Redis更适合作为存储而不是cache。
数据一致性问题:在一致性问题上,个人感觉redis没有memcached实现的好,Memcached提供了cas命令,可以保证多个并发访问操作同一份数据的一致性问题。 Redis没有提供cas 命令,并不能保证这点,不过Redis提供了事务的功能,可以保证一串命令的原子性,中间不会被任何操作打断。
支持的KEY类型:Redis除key/value之外,还支持list,set,sorted set,hash等众多数据结构,提供了KEYS进行枚举操作,但不能在线上使用,如果需要枚举线上数据,Redis提供了工具可以直接扫描其dump文件,枚举出所有数据,Redis还同时提供了持久化和复制等功能。
客户端支持:redis官方提供了丰富的客户端支持,包括了绝大多数编程语言的客户端,比如我此次测试就选择了官方推荐了Java客户端Jedis.里面提供了丰富的接口、方法使得开发人员无需关系内部的数据分片、读取数据的路由等,只需简单的调用即可,非常方便。
数据复制:从2.8开始,Slave会周期性(每秒一次)发起一个Ack确认复制流(replication stream)被处理进度
读写分离:redis支持读写分离,而且使用简单,只需在配置文件中把redis读服务器和写服务器进行配置,多个服务器使用逗号分开如下:
水平动态扩展:历时三年之久,终于等来了期待已由的Redis 3.0。新版本主要是实现了Cluster的功能,增删集群节点后会自动的进行数据迁移。实现 Redis 集群在线重配置的核心就是将槽从一个节点移动到另一个节点的能力。因为一个哈希槽实际上就是一些键的集合, 所以 Redis 集群在重哈希(rehash)时真正要做的,就是将一些键从一个节点移动到另一个节点。
数据淘汰策略:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
 
 
 
 
 
存储方式
Redis内部使用一个redisObject对象来表示所有的key和value。
redisObject最主要的信息:
1⃣️type代表一个value对象具体是何种数据类型(5种数据类型)
2⃣️encoding是不同数据类型在redis内部的存储方式,比如:
type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,
如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串。
3⃣️vm字段,只有打开了Redis的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的。
 
关于 Redis 数据存储的细节,涉及到内存分配器(如 jemalloc)、简单动态字符串(SDS)、5 种对象类型及内部编码、RedisObject。
在讲述具体内容之前,先说明一下这几个概念之间的关系。
下图是执行 set hello world 时,所涉及到的数据模型:
dictEntry:Redis 是 Key-Value 数据库,因此对每个键值对都会有一个 dictEntry,里面存储了指向 Key 和 Value 的指针;next 指向下一个 dictEntry,与本 Key-Value 无关。
Key:图中右上角可见,Key(”hello”)并不是直接以字符串存储,而是存储在 SDS 结构中。
RedisObject:Value(“world”)既不是直接以字符串存储,也不是像 Key 一样直接存储在 SDS 中,而是存储在 RedisObject 中。
实际上,不论 Value 是 5 种类型的哪一种,都是通过 RedisObject 来存储的;而 RedisObject 中的 type 字段指明了 Value 对象的类型,ptr 字段则指向对象所在的地址。
不过可以看出,字符串对象虽然经过了 RedisObject 的包装,但仍然需要通过 SDS 存储。
实际上,RedisObject 除了 type 和 ptr 字段以外,还有其他字段图中没有给出,如用于指定对象内部编码的字段。
jemalloc:无论是 DictEntry 对象,还是 RedisObject、SDS 对象,都需要内存分配器(如 jemalloc)分配内存进行存储。
以 DictEntry 对象为例,有 3 个指针组成,在 64 位机器下占 24 个字节,jemalloc 会为它分配 32 字节大小的内存单元。
下面来分别介绍 jemalloc、RedisObject、SDS、对象类型及内部编码。
 
redis内存管理方式,支持tcmalloc,jemalloc,malloc三种内存分配,memcache使用slabs,malloc等内存分配方式。
简单点,就是redis,是边用边申请,使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配;
memcache使用预分配的内存池的方式,使用slab和大小不同的chunk来管理内存,Item根据大小选择合适的chunk存储,内存池的方式可以省去申请/释放内存的开销
 
数据结构
Redis内部使用一个redisObject对象来表示所有的key和value
redisObject主要的信息包括数据类型(type)、编码方式(encoding)、数据指针(ptr)、虚拟内存(vm)等。
type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部式。
 
struct sdshdr {
int len; //len表示buf中存储的字符串的长度。
int free; //free表示buf中空闲空间的长度。
char buf[]; //buf用于存储字符串内容。
};
 
实际上type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如"20"这样的字符串,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。如果你试图对name进行incr操作则报错。
 
redis的Hash数据类型的key(hash表名称)对应的value实际的内部存储结构为一个HashMap,因此Hash特别适合存储对象。
将整个对象存储在Hash类型中会占用更少内存。
当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
 
 
 
 
Redis的过期策略
Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
 
 
数据淘汰策略
最大缓存配置
  在 redis 中,允许用户设置最大使用内存大小
server.maxmemory
redis.conf中的maxmemory这个值来开启内存淘汰功能
默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
 
maxmemory-policy noeviction
 
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
 
使用策略规则:
1、如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
 2、如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
 
 
高可用
Redis发展过程中的三种模式:主从、哨兵、集群
Redis保证数据可靠性、高可用的思路
 
>>> Redis集群搭建
第一步: 新建redis-cluster文件夹
第二步:复制redis实例
第三步:修改配置文件,修改bin目录下的redis.conf配置文件
修改端口,cluser_enabled = yes
第四部:开启
 
主从复制
slaveof
从服务器连接主服务器,发送 SYNC 命令;
主服务器接收到 SYNC 命名后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令;
主服务器 BGSAVE 执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
 
 
分布式缓存
有于Memcached,对于缓存对象大小有要求,单个对象不得大于1MB,且不支持复杂的数据类型,譬如SET等。因此现在Redis用的越来越多。
<!-- jedis依赖 --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.1</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.6.2.RELEASE</version> </dependency>
 
@Cacheable
@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。
@Cacheable可以指定三个属性,value、key和condition。
 
@CachePut
@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
 
@CacheEvict
是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。
 
redis高可用体现在:主从 / 哨兵 / 集群
常用命令
1⃣️根据配置文件启动redis: src/redis-server redis.confg
2⃣️启动redis客户端:redis-cli -p port
3⃣️Slaver连接Master:slaveof host:ip (测试时使用slaveof手动连接master,正式环境使用配置文件)
4⃣️关闭Redis:shutdown
5⃣️查看主从信息:info Replication
 
主节点挂掉后,手动将S1子节点升级为Master(命令:slaveof no one 手动将Slaver节点升级为Master节点)
 
哨兵
使用哨兵模式,自动监视Master节点,当前挂掉后,自动将Slaver节点变为Master节点
a) sentinel.conf配置文件,修改sentinel monitor host6379 127.0.0.1 6379 1,其它使用默认即可
host6379 主机名称,随便起 主机IP 端口 1表示选举,某个slaver得到超过1票则成成为Master节点
b) 启动sentinel: ./redis-sentinel ../sentinel.conf
 
 
缓存异常
缓存穿透
给不命中的null也做缓存,但是失效时间缩短
一般的缓存系统,都是按照key值去缓存查询,如果不存在对应的value,就应该去DB中查找。由于缓存不命中,每次都要查询持久层。从而失去缓存的意义。
这个时候,如果请求的并发量很大,就会对后端的DB系统造成很大的压力。这就叫做缓存穿透。
关键词:缓存value为空;并发量很大去访问DB
 
造成的原因:1.业务自身代码或数据出现问题;2.一些恶意攻击、爬虫造成大量空的命中,此时会对数据库造成很大压力。
 
解决方法
1.设置布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,
从避免了对底层存储系统的查询压力。
2. 如果一个查询返回的数据为空,不管是数据不存在还是系统故障,我们仍然把这个结果进行缓存,但是它的过期时间会很短
最长不超过5分钟。
 
缓存雪崩
因为缓存层承载了大量的请求,有效的保护了存储 层,但是如果缓存由于某些原因,整体不能够提供服务,于是所有的请求,就会到达存储层,存储层的调用量就会暴增,造成存储层也会挂掉的情况。缓存雪崩的英文解释是奔逃的野牛,指的是缓存层当掉之后,并发流量会像奔腾的野牛一样,大量后端存储。
 
解决方法
(1)设置redis集群和DB集群的高可用,如果redis出现宕机情况,可以立即由别的机器顶替上来。这样可以防止一部分的风险。
(2)使用互斥锁
在缓存失效后,通过加锁或者队列来控制读和写数据库的线程数量。比如:对某个key只允许一个线程查询数据和写缓存,其他线程等待。单机的话,可以使用synchronized或者lock来解决,如果是分布式环境,可以是用redis的setnx命令来解决。
(3)不同的key,可以设置不同的过期时间,让缓存失效的时间点不一致,尽量达到平均分布。
(4)永远不过期:redis中设置永久不过期,这样就保证了,不会出现热点问题,也就是物理上不过期。
(5)资源保护:使用netflix的hystrix,可以做各种资源的线程池隔离,从而保护主线程池。
四种方案,没有最佳只有最合适, 根据自己项目情况使用不同的解决策略。
 
 
缓存与数据库数据一致性
1、分别处理
   针对某些对数据一致性要求不是特别高的情况下,可以将这些数据放入Redis,请求来了直接查询Redis,例如近期回复、历史排名这种实时性不强的业务。而针对那些强实时性的业务,例如虚拟货币、物品购买件数等等,则直接穿透Redis至MySQL上,等到MySQL上写入成功,再同步更新到Redis上去。这样既可以起到Redis的分流大量查询请求的作用,又保证了关键数据的一致性。
 
2、高并发情况下
   此时如果写入请求较多,则直接写入Redis中去,然后间隔一段时间,批量将所有的写入请求,刷新到MySQL中去;如果此时写入请求不多,则可以在每次写入Redis,都立刻将该命令同步至MySQL中去。这两种方法有利有弊,需要根据不同的场景来权衡。
 
3、基于订阅binlog的同步机制
   阿里巴巴的一款开源框架canal,提供了一种发布/ 订阅模式的同步机制,通过该框架我们可以对MySQL的binlog进行订阅,这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。值得注意的是,binlog需要手动打开,并且不会记录关于MySQL查询的命令和操作。
   其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。而canal正是模仿了slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。如下图就可以看到Slave数据库中启动了2个线程,一个是MySQL SQL线程,这个线程跟Matser数据库中起的线程是一样的,负责MySQL的业务率执行,而另外一个线程就是MySQL的I/O线程,这个线程的主要作用就是同步Master 数据库中的binlog,达到数据备份的效果。而binlog就可以理解为一堆SQL语言组成的日志。
 
 
场景一:先更新数据库,再删除缓存
先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。
 
场景二:在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完
可以用队列的去解决这个问,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。
这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。
 
 
 
 
 
 
分布式锁
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
 
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }
 
其他优化
Redis与Memcached的区别与比较
1 、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
2 、Redis支持数据的备份,即master-slave模式的数据备份。
3 、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中
4、 redis的速度比memcached快很多
5、Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的IO复用模型。
附加功能:pub/sub,脚本支持,支持事务
 
 
Redis与ehcache
Redis:属于独立的运行程序,需要单独安装后,使用JAVA中的Jedis来操纵。因为它是独立,所以如果你写个单元测试程序,放一些数据在Redis中,然后又写一个程序去拿数据,那么是可以拿到这个数据的。,
 
ehcache:与Redis明显不同,它与java程序是绑在一起的,java程序活着,它就活着。譬如,写一个独立程序放数据,再写一个独立程序拿数据,那么是拿不到数据的。只能在独立程序中才能拿到数据。
 
优化的参数:
1.设置下redis.conf中的maxmemory选项,我的经验是当你的redis物理内存使用超过内存总容量的3/5时就会开始比较危险了。
2.需要将vm.overcommit设置为1,这参数在大量写入时,很有用
overcommit_memory=0,默认,智能超发,每次要求分配内存时,kernel都会比较请求的空间和空余的空间是否足以分配
overcommit_memory=1,请求分配内存时,永远假装还有足够的内存
overcommit_memory=2,不允许超发内存,即允许分配的大小小于
3.确保设置了一定量的swap,最好和内存一样大,否则内核的OOM(out-of-memory)killer会干掉Redis进程
 
 
 
 
Redis 大量数据插入
使用协议,卢克(Use the protocol, Luke)
原本使用netcat:(cat data.txt; sleep 10) | nc localhost 6379 > /dev/null,但是不能检查错误
pipe mode:cat data.txt | redis-cli --pipe,会有反馈
 
 
 
Redis的性能瓶颈

猜你喜欢

转载自www.cnblogs.com/novalist/p/11621344.html
今日推荐