杂谈
分布式目的是解决并发的问题。每个环节都启用多个节点,哨兵or集群。 分布式是微服务的前提。微服务是把大项目拆分成很多子项目,不同服务可以直接调用,基于什么url协议。 微服务两体系:阿里的duubo、springclould
缓存
mysql也有缓存,但是级别靠后,性能不好。
缓存离客户端越近、离DB越远、越靠前,效果越好。
初识
引入Redis和guawa做缓存
- guawa一级缓存,Redis二级缓存。
- guawa本地缓存,Redis分布式缓存。
guawa:
-
hashmap做的缓存,跟tomcat服务器一体,跟服务器共用缓存,比redis离用户更进一步。
-
分给guawa的内存不能太多,影响服务器性能,一般几百M,一个G。
-
非常热的数据放guawa,次热的放redis。guawa离得近性能更好内存小,redis稍远反应稍慢但内存大,两者结合。
-
并且,两级缓存,起互相备份的功能。
guava缓存使用:
pom.xml 引入guava依赖包
示例:
-
初始化cache,catch接口的引用类型,k是字符串,v是对象,用cachebuilder创建,不能直接new。
-
cachebuilder实例化newbuilder(),其中的方法大部分返回的都是build本身,所以可以引用很长点点点
-
initialCapacity(10)初始化容量10,maximumSize(100)最大容量100个(存100个热门商品),expireAfterWrite(duration:5,TimeUnit.SECONDS)在数据写入缓存以后,5秒过期。build()最后调用build方法返回catch对象 -
cache.put("user","Tom");存入一组数据 -
循环查看cache.getIfPresent(key:"user"),如果catch中user这个key存在,获取对应的value。打印
-
打印以后,
Thread.sleep( millis: 1000);线程睡眠一秒
前五秒user存在,打印,6秒开始,catch中user过期,打印null
后端引入缓存,对前端是透明化的,前端不变。
代码
service/impl/ItemServiceimpl
引入redis分布式(远程)缓存。前面已经配置/初始化好了,只引入就可以。
@Autowired
private RedisTemplate redisTemplate;
引入guava本地缓存,guava是比较轻量级的包,手动处理配置。
private Cache<String, Object> cache;
先查本地缓存有没有,没有再查redis缓存,还没有最后才查mysql。
在对象初始化阶段,创建cache
@PostContruct注解:spring工厂在对象初始化完成/实例化后,调用带这个注解的方法。
private Cache<String, Object> cache;
@PostConstruct
public void init() {
cache = CacheBuilder.newBuilder()
.initialCapacity(10)
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
}
service/impl/ItemService
- 主要优化查询的性能
- 调用的是ItemService接口,里面创建在缓存查询的方法。
- 更多的是替代finditembyid方法
service/impl/ItemServiceimpl
findItemInCache根据id在缓存中取商品,方法的实现:
- 结构与之前类似。
- id小于等于0,参数不合法
- 最后要返回item,先声明
- guava和redis都是kv结构,声明一下key的格式
- 先从guava取,
item = (Item) cache.getIfPresent(key);,取到了非空说明商品在guava一级缓存,直接返回item - 没有,就从redis取。取到了非空说明商品信息在redis(二级缓存)中,把k和v(item商品)存到guava一级缓存中,返回item
- 如果redis中还没有(一二级缓存都没命中),调用finditembyid()方法,直接在mysql中查商品item。查到了item非空,就把商品数据存到一二级缓存中
- guava本地缓存默认有过期时间,redis过期时间设置三分钟
- 扔空,返回null
public Item findItemInCache(int id) {
if (id <= 0) {
throw new BusinessException(PARAMETER_ERROR, "参数不合法!");
}
Item item = null;
String key = "item:" + id;
// guava
item = (Item) cache.getIfPresent(key);
if (item != null) {
return item;
}
// redis
item = (Item) redisTemplate.opsForValue().get(key);
if (item != null) {
cache.put(key, item);
return item;
}
// mysql
item = this.findItemById(id);
if (item != null) {
cache.put(key, item);
redisTemplate.opsForValue().set(key, item, 3, TimeUnit.MINUTES);
return item;
}
return null;
}
controller/itemcontroller
把findItemById方法,改成在缓存中查
@RequestMapping(path = "/detail/{id}", method = RequestMethod.GET)
@ResponseBody
public ResponseModel getItemDetail(@PathVariable("id") int id) {
// Item item = itemService.findItemById(id);
Item item = itemService.findItemInCache(id);
return new ResponseModel(item);
}
略?P8,35min
Redis持久化
redis持久化存点赞数量,可能丢1s的数据,可以接受
RDB持久化:
-
默认持久化方式
-
以全量快照形式,把内存中某个时刻的全部数据转换成二进制后,持久化到硬盘中。RDB文件存的是二进制数据
-
用BGSAVE(后台save)命令触发,(SAVE容易阻塞),手动/自动都可
-
优点:体积小,保存所有数据
-
缺点:不能实时,适合备份。如:每隔6小时执行一次,备份之前的数据 一般用于定时备份数据。 AOF持久化
-
以AOF日志方式存数据,记录每次执行的命令,追加存到AOF文件中
-
优点:实时性好(实时保存数据),保证性能的前提下,可以最多丢失1秒的数据(刷盘频率)
-
缺点:存的是命令,会冗余,体积大 一般用于宕机后重启恢复缓存数据。
RDB-AOF混合持久化
- 从redis4.0开始支持,是基于AOF实现的
- 在AOF重写时,先执行BGSAVE命令生成RDB文件,再把新处理的命令,追加到AOF文件末尾
核心问题:redis的单线程-IO多路复用前提是不能阻塞-持久化容易出现阻塞
RDB持久化机制阻塞问题
- 执行
bgsave,通知redis当前父进程(主进程),fork一个子进程进行持久化,把二进制数据存入RDB文件。(如果都是父进程做,会长时间阻塞)
- 如果当前已经有子进程(已经foge了一个子进程正在持久化,又来了一个bgsave),不创建新进程,返回。
- 父进程fork创建子进程时这一刻,父进程会产生阻塞,无法响应客户端请求。过程比较短暂。
- fork子进程创建完成后,父进程解除阻塞,继续响应其他命令
- 子进程进行持久化:读取父进程内存中的数据(共享父进程数据),转换成二进制后存入(硬盘的)RDB文件。如果已有旧RDB文件,存入新生成的RDB中。3、4步几乎同时进行
- 持久化完成(数据存储完)后,通知父进程替换旧的RDB文件。
问题:子进程在持久化时读取父进程数据,父进程在响应其他命令写数据,如何保证一致性?
- 写时复制/Copy On Write
- 内存最小的管理单元是页(page)
- 子进程持久化时,共享读父进程数据,父进程读写不同页的数据不受影响。
- 父进程可以同时读同页数据
- 改同页的话:先复制一份副本page数据,在副本page上修改。
- 修改以后,副本就是父进程的内存,原page释放空间回收。
快照:
- 快照是数据存储的某一时刻的状态记录。
将内存文件锁住,不能更改。新建文件,把更改都放到新建的文件中。读取时,先读取新建文件,没有再读取锁定的数据。- 共享数据时,把此时内存的数据页锁住,不能修改,只能修改复制(新建)的副本数据。
- 子进程创建好后与父进程共享数据,此时的父进程的数据对子进程来说,就是快照数据。
第五步实现原理:
vim etc/redis.conf查看redis的配置文件。
注:如果运行中redis出现问题,看这个日志文件
- 持久化时RDB文件的文件名。dump.rdb
- 生成的RDB文件的存储路径
- 启动持久化时,redis底层肯定会有个变量持有RDB的文件名和路径。
- 持久化后生成新的RDB文件。加入叫dump1.rdb
- 通知父进程旧RDB作废,引用新的RDB1。就是更新变量的RDB文件名+路径
AOF持久化阻塞问题
关注AOF的重写机制:AOF存命令,数据很大,重复交叉的命令操作会有冗余,重新生成一个压缩(清理冗余)的AOF文件。
- 往Redis中写(增删改) 数据时
- redis把命令缓存到缓冲区(
aof_buf),按一定频率刷到磁盘sync,同步到磁盘的AOF文件
什么时候同步,有几个同步机制,都是操作系统的api,自己调用就行
- 一般定时刷盘,一秒一次
- 掉电死机以后,重启服务要恢复数据,加载AOF文件
redis配置中可以查看
- 是否开启,no没启用
- 启用后aof文件名
AOF存的是命令,数据很大,重复交叉的命令操作会有冗余,恢复时不方便——所以要AOF重写。
AOF重写(服务器自动触发)
缓冲区是操作系统的页缓存
- 重写由
bgrewriteaof命令触发。服务器自动触发 - 前面类似RDB持久化,也需要通知父进程fork一个子进程来持久化新的AOF文件
bgrewriteaof触发重写,如果正在AOF重写,则返回(已经有一个子进程了,就不再做重写)/正在BGSAVE则推迟执行- 父进程fork一个子进程,期间是阻塞的,时间很短。 fork结束解除阻塞,父进程可以继续响应新的写入命令。
- 父进程还拷贝一个当前数据集的快照,给子进程进行重写。
- 子进程读快照数据,获得去掉冗余,去重以后的命令集,写入新的AOF文件。(3、4)和5并行
- 期间如果父进程有新的写命令,父进程把新命令缓存到aof_buf的同时,也同步缓存到
rewrite_buf - 新AOF写入完成,会先把
rewrite_buf重写子线程缓冲池中的数据刷盘sync(操作系统机制)到新AOF中 - 子进程再通知父进程替换旧AOF文件
问题:子进程读快照写入新AOF文件,期间父进程也在响应其他的修改命令,没被快照到的新命令被缓存到aof_buf,再同步到旧AOF。新AOF替换旧,如何防止这部分数据丢失?
解决:引入rewrite_buf缓冲区,防止重写期间丢失新的写入命令
注:重写过程有两个缓冲区,分配内存不大,几M;子进程写入新AOF,数据量很大几十G,rewrite_buf只包含很短时间内的命令几十M,刷盘(操作系统的机制)很快
小结
-
bgsave是存二进制数据,把整个内存数据都存入RDB文件,不能每次执行命令都做bgsave操作,会阻塞父进程。一般五六小时做一次,保证数据完整性,起备份作用。
-
如图AOF白色区域跟bgsave效果等同,能保证数据完整性。区别是AOF每次有写入命令时,都实时同步到AOF文件中。每次实时追加一条命令。
-
AOF的问题是文件存命令,大量的命令会有冗余+体积大,所以有重写机制来压缩AOF文件。隔很久才做一次,不是每次AOF持久化,实时追加命令时就必须做。
-
黄色框都是额外的问题事情,白色区域是持久化主要操作。
问题:如果重写过程中,567步期间断电服务器死机,没有执行到8步。有何影响? 答:对数据完整性无影响。由于旧AOF保存了所有的命令以及重写期间的新命令。重启后加载旧AOF,依然能保证数据完整性。掉电只导致没有得到压缩后的AOF。
======================================================
什么是Redis持久化? Redis持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失;
Redis提供两种持久化机制RDB(默认)和AOF机制;
RDB RDB(Redis DataBase缩写快照)
是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期;记录Redis数据库的所有键值对,在某个时间点将数据写入一个临时文件持久化结束后,用这个临时文件替换上次持久化的文件达到数据恢复;
优点:
- 只有一个文件 dump.rdb,方便持久化;
- 容灾性好,一个文件可以保存到安全的磁盘;
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了Redis的高性能;
- 相对于数据集大时,比 AOF 的启动效率更高;
缺点:
- 数据安全性低;
- RDB 是间隔一段时间进行持久化,如果在持久化时Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;
AOF AOF持久化(即Append Only File持久化)
是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。当两种方式同时开启时数据恢复Redis会优先选择AOF恢复;
优点:
- 数据安全,AOF持久化可以配置 appendfsync 属性,有always属性,每进行一次命令操作就记录到AOF文件中一次;
- 通过append模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题;
- AOF机制的rewrite模式。AOF文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall);
缺点:
- AOF文件比RDB文件大,且恢复速度慢;
- 数据集大的时候,比RDB启动效率低;
RDB和AOF的对比
- AOF文件比RDB更新频率高,优先使用AOF还原数据;
- AOF比RDB更安全也更大;
- RDB性能比AOF好;
- 如果两个都配了优先加载AOF; Redis持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失;
原文链接:blog.csdn.net/weixin_4260…
===================================================
分布式缓存
缓存淘汰策略
数据过期策略(被动淘汰)
- 惰性删除:
- 访问一个key时,redis先检查过期时间,如果已过期立即删除 【性能好,但大量过期key未删除占用空间】
- 定期删除:
- 把设置过期时间的key放入一个独立字典中
- redis默认对字典每100ms扫描一次,检查过期时间,已过期就删除
- 扫描采用贪心策略,每次随机扫描20个key。
如果已过期比例超过25%则再次随机。
内存淘汰策略(内存已满,主动淘汰)
- 当写入数据发现超出maxmemory限制时,采用策略删除
- 八种策略:
- noeviction:直接返回错,不进行内存淘汰
- volatile-ttl:从设置了过期时间的key中,选择过期时间最早的key,淘汰
- volatile-radom:从设置了过期时间的key中,随机选择key,淘汰
- volatile-lru:从设置了过期时间的key中,用LRU算法选择key,淘汰
- volatile-lfu:从设置了过期时间的key中,用LFU算法选择key,淘汰
- allkeys-random:从所有key中,随机选择key,淘汰
- allkeys-lru:从所有key中,用LRU算法选择key,淘汰
- allkeys-lfu:从所有key中,用LFU算法选择key,淘汰
-
LRU算法
- 按照最近最少使用原则筛选数据,筛选出最不常用数据。(比较访问时间)
- 底层用链表实现,当访问/更新/写入key时,放到链表头,末尾就是要淘汰的
- 【不靠谱:如果一个冷key不经常访问,只是刚刚被用过比较新,会被误认为是常用热数据】
-
LFU算法
- 统计访问次数,访问次数最低的数据淘汰
- 若访问次数相同,按LRU淘汰。(比较访问时间,淘汰访问时间更早的数据)
原文链接:blog.csdn.net/a745233700/…
缓存与数据库同步
增/删/改时考虑,读直接读缓存+数据库
-
同步策略:
- 先更新缓存,再更新数据库
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
-
建议方式:
- 先更新数据库,再删缓存
- 第二部失败,采用重试机制解决
为什么是删缓存不是更新?
- 更新缓存不会出现查询未命中,便于读取,但性能消耗大。如果频繁更新但没有读操作,浪费性能。
- 删除缓存操作简便。下次查询缓存,会未命中,再读一次数据库,回填到缓存就行。
假如第二步失败时,先操作缓存还是数据库?
- 先删除缓存再更新数据库
- 数据不同步
- 先更新数据库再删除缓存
- 数据可同步(下会读取数据库回填到缓存),但一些线程会读到旧数据
先删除缓存,再更新数据库 先更新数据库,再删除缓存
原文链接:blog.csdn.net/weixin_4412…
分布式缓存常见问题
缓存穿透
问题: 客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机可能是业务层误将缓存和库中数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。
解决:
- 缓存空对象:存储层未命中后,仍然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回空值
- 布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。底层是好个哈希算法,估算可能是否存在。
缓存击穿
问题:一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。(单个数据过热,挂了以后,局部击穿)
解决:
-
永不过期
- 热点数据不设置过期时间,是“物理”上的永不过期;
- 程序中为每个数据设置逻辑过期时间(redis中不设置过期时间)。当数据逻辑过期时,redis不删缓存,使用单独的线程重建缓存;
-
加互斥锁
- 对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。
这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。??
- 对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。
缓存雪崩
问题:在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。(整个缓存服务宕机,挂了,整体雪崩)
解决:
-
避免数据同时过期
- 设置过期时间时,附加一个随机数,避免大量的key同时过期
-
启用降级和熔断措施
- 发生雪崩时,若应用访问的非核心数据,则直接返回预定义信息/空值/错误信息
- 在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回
-
构建高可用的缓存服务
- 采用哨兵或集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用。