-
为啥要用缓存
当然是为了提高性能和并发啊!结合项目中的业务场景回答即可.
eg.
出单系统中保存了保单信息.这样接口请求就不用每次去查数据库,减轻了数据库的压力,提高了系统的并发。
接下来就可能会根据这个情况问你们怎么处理数据不一致等问题了。 -
缓存和数据库数据不一致问题
涉及到双写问题,就会有数据不一致的情况。需要保存数据一致需要牺牲性能,不过实际场景中一般也不要求这么高的一致性。要求严格一致的话,可以将读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。-
缓存模式: Cache Aside Pattern(先淘汰缓存,再写数据库)
读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应读的时候
写的时候,先删除缓存,然后再更新数据库 -
不一致场景,高并发的情况下出现数据不一致的情况,场景如下:
- 有个写请求,此时删除了缓存中的数据,但是还没来得及写数据库;这个时候来了一个读请求,又把数据库中的旧数据load到缓存中去了,然后上一个请求的写数据库操作完成了。这个时候,数据库中的是最新的数据,缓存中的却是旧数据了。
- 如果是数据库是主从结构,写主库,读从库,那么由于数据库的数据同延时,也是会造成缓存和数据库的数据不一致的问题。
-
解决思路:
针对第一种情况,可以将数据库与缓存更新与读取操作进行异步串行化;将相同id的读写请求hash到同一台服务处理,服务中使用队列一个一个的执行。如果发现队列中有对应资源的写请求,那么就等待其执行结束后再去取值返回(这里需要考虑等待时间过长的问题,还有该方案影响较大,较复杂)。
针对第二种场景,我们用补偿的思维来处理,既然是延迟导致的数据不一致,那么根据数据库实际的延迟时间,我们使用定时任务或者消息触发,如果有写请求结束后,我们在指定的时间之后再删除一次该缓存值,这样即使有不一致的脏数据,那也只会出现在延迟的这一段时间中。
拓展阅读: 缓存架构设计细节二三事
-
-
缓存穿透和雪崩问题
缓存穿透和雪崩就是缓存不可用之后,请求对数据的请求压力都落在了数据库上,导致数据库无法承受如此高的并发,从而导致数据库不可用影响服务的正常运行.
出现该问题需要从多个方面处理:- Redis缓存自身的高可用问题,需要架设多主多从模式,避免全面崩溃;开启RDB和AOF备份,在Redis服务出问题时能快速根据备份文件恢复缓存数据
- 应用服务自身高可用问题,使用hystrix限流&降级保证服务不被拖垮;同时可以增加一层内存缓存,减少对Redis的依赖。
- 还是应用服务层面的,可以过滤部分明显不合理服务的数据请求(eg. 遭遇攻击,所有请求的资源ID都是负数,该资源实际不存在,那么肯定不会命中缓存,就会去数据库查,给数据库造成压力。此时则可以在程序端直接过滤这种情况,或者将其缓存在Redis,只存一个空数据即可)
-
缓存并发竞争问题
问题: 这个也是线上非常常见的一个问题,就是多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了。或者是多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了。
处理思路: 类似JAVA并发处理ABA的问题,给分配一个递增的序列号之类的,序列号在当前值之前的写操作就不执行. -
Redis为啥这么快(为啥redis单线程模型也能效率这么高)
Redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler。这个文件事件处理器,是单线程的,redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件,为啥快呢:
1)纯内存操作
2)核心是基于非阻塞的IO多路复用机制
3)单线程反而避免了多线程的频繁上下文切换问题(这个感觉就emm…)
4)内部数据结构设计 -
Redis持久化方式及其区别
RDB: 通过fork一个子进程保存当前内存的一个快照实现备份. 适合大规模的数据恢复,但是数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。另外备份时会占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件.# save <指定时间间隔> <执行指定次数更新操作>,满足条件就将内存中的数据同步到硬盘中 save <seconds> <changes> # 指定本地数据库文件名,一般采用默认的 dump.rdb dbfilename dump.rdb # 默认开启数据压缩 rdbcompression yes
AOF: 通过对每条写入命令以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。 AOF对数据数据的完整性和一致性支持更好,但是其备份日志文件一般会比RDB方式的备份文件更大,恢复也更慢,同时由于fsync的频率方式,会影响Redis的性能。
# 打开aof appendonly yes # 日志文件 appendfilename "appendonly.aof" # 更新条件 # appendfsync always appendfsync everysec # appendfsync no # 触发重写的配置 # 时间长了日志会特别大,此时需要触发重写 # 重写的原理:Redis 会fork出一条新进程,读取内存中的数据,并重新写到一个临时文件中。最后替换旧的aof文件。 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
-
Redis基本类型及其内在数据结构
主要有String、List、Hash、Set、zSet五种
其内在数据结构定义如下:
-
Redis如何实现分布式和高可用(eg. 主从模式的数据复制、hash分槽等)
Redis Cluster实现了分布式.
主从结构和数据持久化机制保证了高可用.
Redis Cluster原理流程:
hash分槽,
//todo: 流程实现
Redis主从模式原理:
Redis 2.8之前主从模式,主从之间的数据复制只有全复制机制,通过执行命令SLAVEOF ip port
,使得其成为从服务器。全数据复制流程如下:
一. 数据复制- 从服务器向主服务器发送SYNC命令
- 主服务器收到SYNC请求后,执行BGSAVE命令在后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的写命令。
- 主服务器将生成的RDB文件发送给从服务器,从服务器根据RDB文件更新状态
- 主服务器将缓冲区中的写命令发送给从服务器,从服务器执行这些命令,最终和主服务器达到一致的状态
二. 命令传播
6. 数据复制完成后,为了保持一致的状态,主服务的写命令都需要传播给其从服务器。不足: 上面的方式存在不足,即如果从服务器是和主服务器断开后,又立马重新连接上后。那么此时如果还执行全同步的话,就十分浪费。因为全同步非常占用CPU,IO和带宽等。
所以Redis 2.8之后,支持了PSYNC,即部分重同步机制
部分重同步机制主要有依靠:
主从服务器的复制偏移量: 用于记录主从服务器的状态是否一致,以及数据复制时的偏移量
主服务器的复制积压缓冲区:固定大小的(默认1M)FIFO队列,用于记录主服务器的写命令
主服务器的ID: 用于判断,从服务器断开连接后重新连接到的主服务器还是不是原来那个部分重同步流程如下:
主从服务器都有一个复制偏移量,当执行了写命令时,复制偏移量对应会增加。- 从服务器请求同步,带上当前的offset和之前的主服务器ID
- 如果请求中的主服务ID和当前的主服务器ID不一致,或者其offset的值已经不再复制积压缓冲区内,那么需要执行全同步,流程同上;
- 否则,执行部分同步,主服务器将复制积压缓冲区中offset之后的命令发送给从服务器;
- 从服务器执行对应写命令,并修改offset,达到和主服务器状态一致
-
Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现?
设置了定时过期的会自动过期,没有设置的如果内存不够则按照最少使用来删除。Redis删除策略如下:定期删除: 默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除
惰性删除:在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西但是仅仅这样的话,会有如下问题:
定期删除时,部分key没有被删除。而且,这部分key也没有访问 。那么这部分数据岂不是就一直在占用内存。这个时候就会走内存删除策略,有如下几种:noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除java实现LRU的话:
public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int maxSize; public LRUCache(int maxSize){ this(maxSize, 16, 0.75f, false); } public LRUCache(int maxSize, int initialCapacity, float loadFactor, boolean accessOrder){ super(initialCapacity, loadFactor, accessOrder); this.maxSize = maxSize; } // 最重要就是重写该方法咯,就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据 @Override protected boolean removeEldestEntry(Map.Entry eldest) { return this.size() >this.maxSize; } }
-
使用Redis遇到过什么问题
这个问题就需要根据自己项目来实际说一个.比如这边遇到过一个热点问题。一般处理方式:-
增加内存缓存,减轻Redis的压力
-
分散热点数据
问题发现和处理:老一套方案,首先定位性能瓶颈: CPU/内存/IO等
-
ps aux | grep appname 查看进程ID
-
top -H pid查看cpu和内存的使用情况(未发现应用服务器的cpu和内存有较高的使用率)
cpu繁忙一般可能的情况如下:- 线程中的不合理循环
- 发生了频繁的full gc
- 多线程的上下文频繁切换
-
通过Arthas工具查看接口执行过程中每个方法的耗时情况
$trace xxx #cost
方法内部调用路径,并输出方法路径上的每个节点上耗时, trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。trace 能方便的帮助你定位和发现因 RT 高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路结果如下:
+---[0.001162ms] com.paic.ibcsapi.service.dto.ApplyResultDTO:setApplyPolicyNo() +---[0.511972ms] com.xxx.service.apply.save.ApplySaveService:fillApplyPolicyNo() +---[19.709352ms] com.xxx.service.apply.save.ApplySaveService:saveContractDTOInfo() +---[1.313593ms] com.xxx.service.apply.save.ApplySaveService:saveApplyParameterInfo() +---[min=4.35E-4ms,max=9.48E-4ms,total=0.002326ms,count=3] java.lang.StringBuilder:<init>() +---[min=5.1E-4ms,max=0.00146ms,total=0.004705ms,count=6] java.lang.StringBuilder:append() +---[0.001361ms] com.paic.icore.acss.base.dto.BaseInfoDTO:getTransactionNo() +---[min=4.86E-4ms,max=6.61E-4ms,total=0.001777ms,count=3] java.lang.StringBuilder:toString() +---[523.544075ms] com.xxx.common.service.redis.RedisService:stringSet() +---[9.91E-4ms] com.xxx.common.util.StringUtils:isNotBlank() +---[min=6.07E-4ms,max=7.75E-4ms,total=0.001382ms,count=2] java.lang.String:equals() +---[min=5.03E-4ms,max=6.95E-4ms,total=0.001198ms,count=2] java.lang.Integer:intValue() +---[min=592.763104ms,max=868.853641ms,total=1461.616745ms,count=2] com.xxx.common.service.redis.RedisService:ObjectSet()
-
通过trace 定位到是redis耗时后,定位redis问题
- 查看redis服务器状态,发现压测时有台服务器的 查看redis服务器状态,发现压测时有台服务器的CPU达到70%左右
- slowlog get 命令获取redis慢查询定位到是某个key的读取导致, 到此发现是redis数据的热点问题
-
解决方案
- 出现该问题是有工具类滥用redis的问题,每次获取数据都从redis获取。首先,调整代码,增加内存缓存。redis缓存刷新时发布事件动态刷新内存缓存; 然后,优化存储的dto,只缓存必要的数据。减小数据量;最后,替换序列化工具,将jackson序列化替换为fastjson.
- 流程中部分耗时的IO操作线程异步后台处理
-
-
Redis热点数据问题
热点数据分类:- 用户消费的数据远大于生产的数据(热卖商品、热点新闻、热点评论、明星直播)。
- 请求分片集中,超过单 Server 的性能极限。
热点数据危害:
- 流量集中,达到物理网卡上限。
- 请求过多,缓存分片服务被打垮。
- DB 击穿,引起业务雪崩。
解决方案:
- 读写分离
- 服务内存缓存
推荐阅读《redis设计与实现》
待续