Redis介绍
目录Redis初识篇什么是Redis?为什么要用Redis?是不是使用缓存就一定好呢?Redis实战篇Redis的使用jedis客户端redisson客户端lettuce客户端Jedis客户端和Redisson客户端比较缓存的误用使用Redis缓存时出现的异常Redis分布式锁Tair的实现Tendis总结Redis初识篇什么是Redis?Redis(Remote Dictionary Serve
目录
Redis初识篇
什么是Redis?
Redis(Remote Dictionary Server)是一个开源的数据存储系统,它可以用作数据库、缓存和消息中间件。支持多种数据结构,如字符串(String)、散列(Hashes)、列表(Lists)、集合(Sets)、有序集合(Sorted Sets)与范围查询,bitmaps、hyperloglogs和地理空间(geospatial)索引半径查询。Redis内置了复制(Replication),LUA脚本(Luascripting),LRU驱动事件(LRU eviction),事物(Transactions)和不同级别的磁盘持久化(Persistence),并通过Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。
上面这段话是官网的定义,对于没有了解过Redis的小伙伴们,我们可以一点一点的去理。数据库我们大家都用过,最常用的mysql也是关系型数据库的一种,我们现在大部分的web应用的底层数据持久化都是使用的mysql。Redis是非关系型数据库中的一种,也有称它为键值存储数据库。它是可以用作数据库来使用,但是作为内存存储有一个特点就是断电即失。使用RDB持久化存储可以把数据以快照的形式保存到本地磁盘,但如果在较复杂的业务需求上的话,使用关系型数据库的效果会更好。
而缓存在广义上是指数据高速交换的存储介质用于数据的更快速访问,狭义上是指加速CPU交换的存储器。当用户查询数据,首先在缓存中寻找,如果找不到才会去数据库中查找。缓存的本质就是用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。缓存的性能一般比DB高50倍到100倍以上。Redis作为key-value的非关系型数据库最合适作为缓存使用。
消息中间件(Active Messenger)利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。常见的消息中间件有JMS、AMQP、Kafka、RabbitMQ、RocketMQ、ActiveMQ、ZeroMQ、MetaMQ等,最主要的几个用途是发布订阅系统的聊天室,秒杀活动下的流量削峰,大量日志数据的处理。对比RPC,RPC是典型的同步方式,而消息中间件属于异步方式。RPC的调用强依赖于上层服务,而MQ不是强依赖于其他服务。部分数据库如 Redis、MySQL 以及 PhxSQL 也可实现消息队列的功能。
为什么要用Redis?
Redis通常用来用作缓存。有的人可能在想只有Redis可以做缓存,mysql就没有缓存吗?事实上mysql也是有缓存的,在执行Sql语句时会将可以作为缓存的sql结果保存在查询缓存(query cache)中,这样有相同的sql查询语句执行时,可以不用sql解析、优化器优化直接从查询缓存中拿到结果。
但是使用查询缓存会有以下几个问题:
1、查询缓存只在本地,如果是多台服务器,sql只会发送到其中一台,这样本地缓存命中率较低,这样本地缓存的作用就不明显了。
2、从查询缓存中取出查询结果或将查询结果添加进去会给服务器带来额外的性能损耗。
3、当表结构发生变化时,这个表的缓存查询将不再有效。
Redis官网性能测试数据表明Redis读的速度是11万次/秒,写的速度是8.1万次/秒。读写能力远大于mysql。
是不是使用缓存就一定好呢?
使用缓存会存在以下几个问题,
1、缓存与数据库双写不一致
是先更新缓存还是先写数据库,如果顺序不对可能会造成脏数据。更新缓存的设计模式有四种Cache Aside, Read Through, Write Through, Write Behind Caching。标准的模式是旁路缓存(Cache Aside),先从Cache里取数据,没有取到数据则从数据库取,成功从数据库取到后再回存到缓存中。更新时先把数据存到数据库中,更新成功后再让缓存失效。
2、缓存雪崩、缓存穿透
如果瞬间有大量用户请求数据库,可能会导致查询数据库非常缓慢,甚至会造成数据库挂了的严重后果。而在redis启动起来后,数据没有提前加载到redis里面,所有用户都是访问mysql。缓存雪崩可能是因为数据未加载到缓存中,或者缓存同一时间大面积的失效(宕机、cache服务挂了或者不响应了),从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。正确的做法是缓存预热。
缓存穿透是指缓存和数据库中都没有的数据,但用户不断发起请求,这时可能会导致数据库因压力过大而宕机。比如攻击者一直请求一个不存在的数据,解决方案我们可以使用布隆过滤器。
3、缓存并发竞争
缓存并发竞争的产生原因是多个子系统set同一个key,导致产生并发竞争。解决方案有:1)使用分布式锁。2)将key对应的值带上时间戳。3)放入队列中,串行set key。
Redis实战篇
Redis的使用
目前使用的Redis的Java实现客户端主要是Jedis和Redisson以及Lettuce。贝壳基于性能考虑(压测)选型Lettuce客户端并封装了Kedis工具包作为Redis客户端集群方案。
Jedis使用阻塞的I/O,其方法调用都是同步的,不支持异步,Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以操作单个Redisson连接来完成各种操作。
Redis的使用较为简单,导入客户端的依赖包,里面会有自动配置类,注入所配置的信息后再使用客户端即可。
Lettuce是高级Redis客户端,用于线程安全同步、异步和响应使用,底层集成了Project Reactor提供的反应式编程,和Redisson一样使用非阻塞的I/O和集成Netty框架。它封装同步、异步、交互API,不执行阻塞和事务操作时,多线程可共享连接,极大缓解Redis维护连接数的压力。
jedis客户端
/**
* 把键值对(key,value)存入到redis中,setex命令如果key存在,则会替换旧的值
*/
Jedis jedis = jedisPool.getResource();
String result = jedis.setex(key, timeToLive, value);
/**
* 从redis中取出key的值
*/
Jedis jedis = jedisPool.getResource();
String result = jedis.get(key);
上面是最基本的用法,通常会使用AOP技术通过@Aspect+@Around注解找到切面连接点的名字和所在类名进行处理使用,下面是使用Jedis连接池代理工厂获取客户端实例,不过需要注意的是redis中存入的值需要序列化。
@Aspect
public class AOPConfig {
@Around(@annotation(元注解类名路径))
public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
... ...
JedisPoolProxy jedis = client.getJedisPoolProxy(clientId);
byte[] arr = jedis.get(key);
... ...
if(不存在){
... ...
//注意这里的value必须序列化,value可以是对象序列化转成byte数组存进去
jedis.set(key,value);
jedis.expire(key, timeToLive);
}
}
redisson客户端
而Redisson使用起来更为简便,导入依赖后直接使用RedisClient即可,当然也可以利用AOP封装成注解形式。
RBucket<Object> rBucket = redissonClient.getBucket(clientId);
Object result = rBucket.get();
//注意这里的value可以是字节流也可以是对象,但必须序列化。
rBucket.set(value, timeToLive, TimeUnit.SECONDS);
还有个需要注意的地方是应用不要过度依赖缓存
我们一般不会要求缓存服务器的更新和数据库的更新在同一个事物内,所以肯定有概率缓存和数据库不一致的情况,所以数据的最终一致性最好不要依赖缓存,可以在应用层和或者数据库CAS的方式增加校验,另外应用也不应该严重依赖缓存,当缓存服务器挂掉之后至少要保证服务能够在没有高并发情况下继续正常对外提供服务,举个例子,
lettuce客户端
lettuce客户端的配置基本上和jedis客户端或Redisson客户端差不多
###### redis ######
redis:
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
host: ${redis.basicHost}
port: ${redis.basicPort}
password: ${redis.baiscPwd}
避免强依赖缓存
获取城市列表接口使用缓存时是在程序内部使用的,如果缓存服务器挂掉,这个接口也就无法使用,所以缓存切面类的那种方式在redis服务异常时直接调用代理方法 ProceedingJoinPoint.proceed()。
@Override
public Message<Result> getAllCities() {
String result = RedisUtil.get(Constants.REDIS_CACHE_ID, "cache_value");
/**
* 业务代码
*/
ResultSet resultSet = getResultSet();
RedisUtil.set(Constants.REDIS_CACHE_ID, "cache_value", JSON.toJSONString(resultSet), 60);
return getOkResult(resultSet);
}
Jedis客户端和Redisson客户端比较
Redisson相比于Redis功能较为简单,不支持字符串操作,不支持排序,事务,管道,分区等Redis特性,但实现了分布式和可扩展的Java数据结构。Redisson分布式锁有可重入锁、联锁、真分布式锁、公平锁、读写锁等。尝试加锁设置一个过期时间是为了防止死锁现象的发生,如果锁过期时间到了,任务还没有执行完,那么watch dog就会起作用,延长key的生存时间。
RLock lock = redissonClient.getLock(key);
缓存的误用
1、使用缓存如果是多服务共用缓存实例,它可能存在的问题是key冲突,各个服务彼此冲掉对方的数据。并且,各个服务吞吐量不同,共用一个实例可能容易把另外一个服务的热数据挤出去。还会导致服务之间的耦合。
2、如上图,微服务架构中,服务提供方缓存数据(可以向调用方屏蔽获取数据的复杂性),服务调用方也缓存数据,这个其实是有问题的。假如服务修改db里的数据,淘汰了服务cache之后,难以通知调用方淘汰其cache里的数据,从而导致数据不一致。
3、把缓存作为服务与服务之间传递数据的媒介,比如把发送短信记录存到缓存中去(这个就是误用)。MQ能支持push,而cache只能拉取,不实时,有时延。最好让专业的软件干专业的事:nginx做反向代理,db做固化,cache做缓存,mq做通道。
4、使用缓存未考虑雪崩:如果缓存挂掉,所有请求会压到数据库,导致整个系统不可服务。常见方案有,1、高可用缓存集群:一个缓存实例挂掉后,能够自动做故障迁移。2、缓存水平切分(推荐一致性Hash算法进行切分):一个缓存实例挂掉后,不至于所有流量都压到数据库上。
使用Redis缓存时出现的异常
1、Redis中存有不同的数据类型的数据,但是取值时使用相同的Key值,比如已存在叫demo的RBucket数据对象,然后你取值的时候又用demo作为key去取RLock的对象,那么就会抛出异常,异常信息:WRONGTYPE Operation against a key holding the wrong kind of value。
2、Hot key。对于大多数互联网系统,数据是分冷热的。比如最近的新闻、新发表的微博被访问的频率最高,而比较久远的之前的新闻、微博被访问的频率就会小很多。而在突发事件发生时,大量用户同时去访问这个突发热点信息,访问这个 Hot key,这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象,甚至会被 Crash。
原因分析:Hot key 引发缓存系统异常,主要是因为突发热门事件发生时,超大量的请求访问热点事件对应的 key,比如微博中数十万、数百万的用户同时去吃一个新瓜。数十万的访问请求同一个 key,流量集中打在一个缓存节点机器,这个缓存机器很容易被打到物理网卡、带宽、CPU 的极限,从而导致缓存访问变慢、卡顿。
解决方案:找到热 key 后,就有很多解决办法了。
1)可以将这些热 key 进行分散处理,比如一个热 key 名字叫 hotkey,可以被分散为 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,这 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey,这样就可以把热 key 的请求打散,避免一个缓存节点过载。
2)也可以 key 的名字不变,对缓存提前进行多副本 + 多级结合的缓存架构设计。
3)如果热 key 较多,还可以通过监控体系对缓存的 SLA 实时监控,通过快速扩容来减少热 key 的冲击。
4)业务端还可以使用本地缓存,将这些热 key 记录在本地缓存,来减少对远程缓存的冲击。
Redis分布式锁
在分布式系统中,锁对数据一致性和准确性也至关重要。分布式系统相同的服务部署到不同的服务器上,分布式多进程服务不在同一个物理服务器上,因此不能共享堆内存,会产生数据之间不能共享而产生数据一致性问题。
分布式锁的实现可以基于数据库实现,也可以基于redis的实现
基于数据库实现是不同机器上的不同服务处理的是同一个数据库,数据库对他们来说是可见的。
乐观锁机制其实就是在数据库表中引入一个版本号字段来实现,当从数据库中读取数据时同时也把version读取出来;悲观锁机制是更新的时候先加锁,其他线程就不能进行更新。
基于Redis的实现是在redis设置锁超时时间,如果redis中不存在该key,就加锁成功。
Redis的分布式缓存特性使其成为了分布式锁的一种基础实现。通过Redis中是否存在某个锁ID,则可以判断是否上锁。为了保证判断锁是否存在的原子性,保证只有一个线程获取同一把锁,Redis有SETNX(即SET if Not
eXists)和GETSET(先写新值,返回旧值,原子性操作,可以用于分辨是不是首次操作)操作。
为了防止主机宕机或网络断开之后的死锁,Redis没有ZK那种天然的实现方式,只能依赖设置超时时间来规避。
以上是Redis的一种常见的实现方式,除此以外还可以用SETNX+EXPIRE来实现。Redisson是一个官方推荐的Redis客户端并且实现了很多分布式的功能。它的分布式锁就提供了一种更完善的解决方案,源码:
https://github.com/mrniko/redisson。
Tair的实现
Tair和Redis的实现类似,Tair客户端封装了一个expireLock的方法:通过锁状态和过期时间戳来共同判断锁是否存在,只有锁已经存在且没有过期的状态才判定为有锁状态。在有锁状态下,不能加锁,能通过大于或等于过期时间的时间戳进行解锁。
采用这样的方式,可以不用在Value中存储时间戳,并且保证了判断是否有锁的原子性。更值得注意的是,由于超时时间是由Tair判断,所以避免了不同主机时钟不一致的情况。
以上的几种分布式锁实现方式,都是比较常见且有些已经在生产环境中应用。随着应用环境越来越复杂,这些实现可能仍然会遇到一些挑战。
强依赖于外部组件:分布式锁的实现都需要依赖于外部数据存储如ZK、Redis等,因此一旦这些外部组件出现故障,那么分布式锁就不可用了。
无法完全满足需求:不同分布式锁的实现,都有相应的特点,对于一些需求并不能很好的满足,如实现公平锁、给等待锁加超时时间等。
基于以上问题,结合多种实现方式,致力于提供灵活可靠的分布式锁又提出了Cerberus分布式锁。
Cerberus分布式锁使用了多种引擎实现方式(Tair、ZK、未来支持Redis),支持使用方自主选择所需的一种或多种引擎。这样可以结合引擎特点,选择符合实际业务需求和系统架构的方式。
Cerberus分布式锁将不同引擎的接口抽象为一套,屏蔽了不同引擎的实现细节。使得使用方可以专注于业务逻辑,也可以任意选择并切换引擎而不必更改任何的业务代码。
Tendis
Tendis是腾讯互娱CROS DBA团队 & 腾讯云数据库团队自主设计和研发的分布式高性能KV存储数据库,兼容Redis核心数据结构与接口,可提供大容量、低成本、强持久化的数据库能力,适用于兼容Redis协议、需要大容量且较高访问性能的温冷数据存储场景。
腾讯造出新的轮子肯定是为了解决某些问题或者满足某些特定的需求。在集群架构上Tendis使用去中心化集群架构,每个数据节点都拥有全部的路由信息,用户可以访问集群中的任意节点,并且通过redis的move协议,最终路由到正确的节点。
看其介绍主要是为了解决原生Redis固有的fork问题而预留部分内存问题。
简单介绍一下fork问题,Redis的持久化机制有RDB(Redis DataBase)和AOF(Append Only File),主线程会fork一个子线程进行备份保存的工作,但是fork同步操作很快,如果同步的数据量太大,fork阶段就会发生阻塞,可用命令 info:latest_fork_usec 查看redis持久化所花费的时间,如若redis的访问量QPS过高,持久化时间太长就会严重阻塞redis。
改善fork的方法有:1、升级硬件。2、控制redis最大可用内存(maxmemory)。3、合理配置内存分配策略(vm.overcommit_memory=1)。4、降低fork频率
总结
虽然在大数据量的访问下Lettuce性能会更好,但是Redis客户端的选型不是仅仅只看性能测试,还需要考虑不同的应用场景,其他场景下Jedis或者Redisson客户端性能可能会更好。就如早期的Diffie–Hellman加密算法虽然已经过去了几十年,但现在仍然有场景在使用。但是要使用好缓存,首先需要了解不同的方案或者技术,其次是通过实战了解区别,才能大概的知道如何去更好的解决问题。
补充:Redisson中还有布隆过滤器,布隆过滤器相当于概率型的数据结构,将元素通过Hash函数映射到阵列中的某个点。利用非100%的正确率换取空间复杂度的进一步缩小。感兴趣的小伙伴可以自己去了解一下。
参考文献:
1、https://blog.csdn.net/hualaoshuan/article/details/102638188
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)