42

架构设计 | 缓存管理模式,监控和内存回收策略

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU4Njg0MzYwNw%3D%3D&%3Bmid=2247484584&%3Bidx=1&%3Bsn=e22b93e6e81d9ba611fc5b220ee9eb5b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、缓存设计

1、缓存的作用

在业务系统中,查询时最容易出现性能问题的模块,查询面对的数据量大,筛选条件复杂,所以在系统架构中引入缓存层,则是非常必要的,用来缓存热点数据,达到快速响应的目的。

缓存使用的基本原则:

  • 所有缓存数据,必须设置过期时间;

  • 核心业务流程不通过缓存层;

  • 缓存层移除,不影响现有流程;

  • 系统各个端首页数据不实时查询;

  • 报表数据不实时查询加载;

  • 归档数据(定时统计的结果数据)不实时查询;

这里是业务架构中常用的缓存策略,缓存通过牺牲强一致性来提高性能,所以并不是所有的业务都适合用缓存,实际考量都会针对具体的业务,比如用户相关维度的数据修改频率低,会使用缓存,但是用户权限数据(比如:免费次数)会考虑实时校验,缓存层使用的相对较少。

2、缓存设计模式

Cache-Aside模式

业务中最常用的缓存层设计模式,基本实现逻辑和相关概念如下:

ZBbqAnF.png!web

  • 缓存命中:直接查询缓存且命中,返回数据;

  • 缓存加载:查询缓存未命中,从数据库中查询数据,获取数据后并加载到缓存;

  • 缓存失效:数据更新写到数据库,操作成功后,让缓存失效,查询时候再重新加载;

  • 缓存穿透:查询数据库不存在的对象,也就不存在缓存层的命中;

  • 缓存击穿:热点key在失效的瞬间,高并发查询这个key,击穿缓存,直接请求数据库;

  • 缓存雪崩:缓存Key大批量到过期时间,导致数据库压力过大;

  • 命中率:缓存设计的是否合理要看命中率,命中率高说明缓存有效抗住了大部分请求,命中率可以通过Redis监控信息计算,一般来说命中率在(70-80)%都算合理。

    并发问题

执行读操作未命中缓存,然后查询数据库中取数据,数据已经查询到还没放入缓存,同时一个更新写操作让缓存失效,然后读操作再把查询到数据加载缓存,导致缓存的脏数据。

在遵守缓存使用原则下出现该情况概率非常低,可以通过复杂的Paxos协议保证一致性,一般情况是不考量该场景的处理,如果缓存管理过于复杂,会和缓存层核心理念相悖。

基本描述代码:

@Service
public class KeyValueServiceImpl extends ServiceImpl<KeyValueMapper, KeyValueEntity> implements KeyValueService {

@Resource
private RedisService redisService ;

@Override
public KeyValueEntity select(Integer id) {
// 查询缓存
String redisKey = RedisKeyUtil.getObectKey(id) ;
String value = redisService.get(redisKey) ;
if (!StringUtils.isEmpty(value) && !value.equals("null")){
return JSON.parseObject(value,KeyValueEntity.class);
}
// 查询库
KeyValueEntity keyValueEntity = this.getById(id) ;
if (keyValueEntity != null){
// 缓存写入
redisService.set(redisKey,JSON.toJSONString(keyValueEntity)) ;
}
// 返回值
return keyValueEntity ;
}

@Override
public boolean update(KeyValueEntity keyValueEntity) {
// 更新数据
boolean updateFlag = this.updateById(keyValueEntity) ;
// 清除缓存
if (updateFlag){
redisService.delete(RedisKeyUtil.getObectKey(keyValueEntity.getId()));
}
return updateFlag ;
}
}

Read-Throug模式

当应用系统向缓存系统请求数据时,如果缓存中并没有对应的数据存在,缓存系统将向底层数据源的读取数据。如果数据在缓存中存在,则直接返回缓存中存在的数据。把更新数据库的操作由缓存层代劳了。

Write-Through模式

更新写数据时,如果没有命中缓存,则直接更新数据库,如果命中了缓存,则先更新缓存,然后由缓存系统自行更新数据库。

Write-Behind模式

应用系统对缓存中的数据进行更新时,只更新缓存,不更新数据库,缓存系统会异步批量向底层数据源更新数据。

二、数据一致问题

业务开发模式中,会涉及到一个问题:如何最大限度保证数据库和Redis缓存的数据一致性?

首先说明一下:数据库和缓存强一致性同步成本太高,如果追求强一致,缓存层存在的价值就会很低,如上缓存模式一中几乎可以解决大部分业务场景问题。

解决这个问题的方式很多:

ZVZf63V.png!web

方案一说明:

  • 数据库更新写入数据成功;

  • 准备一个先进先出模式的消息队列;

  • 把更新的数据包装为一个消息放入队列;

  • 基于消息消费服务更新Redis缓存;

分析:消息队列的稳定和可靠性,操作层面数据库和缓存层解耦。

方案二说明:

  • 提供一个数据库Binlog订阅服务,并解析修改日志;

  • 服务获取修改数据,并向Redis服务发送消息;

  • Redis数据进行修改,类似MySQL的主从同步机制;

分析:系统架构层面多出一个服务,且需要解析MySQL日志,操作难度较大,但流程上更为合理。

总结描述

分布式架构中,缓存层面的基本需求就是提高响应速度,不断优化,追求数据库和Redis缓存的数据快速一致性,从提供的各种方案中都可以看出,这也在增加缓存层面处理的复杂性,架构逻辑复杂,就容易导致程序错误,所以针对业务选择合理的处理逻辑,这点很关键。

三、缓存监控

1、Redis服务监控

通过info命令查看Redis服务的参数信息,可以通过传参查看指定分类配置。通过config..set设置具体配置参数。例如:

@Override
public Properties info(String var) {
if (StringUtils.isEmpty(var)){
return redisTemplate.getRequiredConnectionFactory().getConnection().info();
}
return redisTemplate.getRequiredConnectionFactory().getConnection().info(var);
}

传参说明:

  • memory:内存消耗相关信息

  • server:有关Redis服务器的常规信息

  • clients:客户端连接部分

  • stats:一般统计

  • cpu:CPU消耗统计信息

应用案例:

@RestController
public class MonitorController {

@Resource
private RedisService redisService ;

private static final String[] monitorParam = new String[]{"memory","server","clients","stats","cpu"} ;

@GetMapping("/monitor")
public List<MonitorEntity> monitor (){
List<MonitorEntity> monitorEntityList = new ArrayList<>() ;
for (String param:monitorParam){
Properties properties = redisService.info(param) ;
MonitorEntity monitorEntity = new MonitorEntity () ;
monitorEntity.setMonitorParam(param);
monitorEntity.setProperties(properties);
monitorEntityList.add(monitorEntity);
}
return monitorEntityList ;
}

}

通过上述参数组合,把Redis相关配置参数打印出来,然后可视化输出,俨然一副高端的感觉。

配置参数说明:

这里只对两个参数说明一下,计算命中率的关键信息:

  • keyspace_misses:查找缓存Key失败的次数;

  • keyspace_hits:查找缓存Key命中的次数;

公式:命中率=命中次数/(hits+misses)查找总次数。

2、LRU算法说明

Redis的数据是放在内存中的,所以速度快,自然也就受到内存大小的限制,如果内存使用超过配置,Redis有不同的回收处理策略。

内存模块参数:maxmemory_policy

  • noenviction:不回收数据,查询直接返回错误,但可以执行删除;

  • allkeys-lru:从所有的数据中挑选最近最少使用的数据淘汰;

  • volatile-lru:已设置过期时间的数据中挑选最近最少使用的数据淘汰;

  • allkeys-random:从所有数据中任意选择数据淘汰;

  • volatile-random:从已设置过期时间的数据中任意选择数据淘汰;

  • volatile-ttl:从已设置过期时间的数据中挑选将要过期的数据淘汰;

大部分情况下,业务都是希望最热点数据可以被缓存,所以相对使用allkeys-lru策略偏多。这里要根据业务模式特点衡量。

四、源代码地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent

MzYveqr.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK