1、背景
在2021年,某地系统面临了一次前所未有的挑战:16万用户在短短10分钟内涌入系统。这一突如其来的高并发请求远超过了系统预期,导致服务器遭受了极大的压力。当时,系统依赖的是一个单节点的Redis缓存,在平常的业务量下似乎运行得还算顺畅;然而,在如此极端的情况下,它显得力不从心。具体表现为用户在尝试填报志愿时,频繁遇到了如下错误信息:redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
。这一错误直接导致了一个严重的问题------Redis连接池的资源耗尽,无法应对更多的并发请求。
在业务量较低时,依赖单点的Redis或Ehcache作为缓存策略,似乎可以满足需求,可以有效地减少了对数据库的直接访问,提高了数据处理的速度。随着系统对稳定性和缓存一致性的需求提高,采用Redis集群成了一个可行的选择。Redis集群通过多节点提升了系统的可用性和缓存数据的一致性。
然而,即便Redis作为一个高性能的缓存解决方案能够在许多情况下提供出色的表现,我们也必须面对一个现实------内存是非常宝贵的资源,且成本昂贵。在追求缓存数据共享的同时,可能会因为资源的高昂成本而感到头疼。就像缓存可以作为数据库的"护卫"一样,我们也需要为Redis找到一个可靠的"护卫"。
2、流行缓存解决方案对比
在挑选合适的缓存方案时,软件架构师和开发人员需从应用的实际需求、性能指标、成本预算及运维复杂性等多角度进行全面考量。
市面上主流的缓存技术包括Ehcache、Caffeine、Spring Cache、Redis、J2Cache、Memcached和Guava Cache等,它们各自适用于不同的使用场景。在进行选择时,要对这些技术进行全方位的对比和分析。
缓存框架 | 类型 | 数据一致性 | 性能 | 易用性 | 功能特性 | 适用场景 |
---|---|---|---|---|---|---|
Ehcache | 本地缓存 | 应用逻辑维护 | 高 | 高 | 丰富的本地缓存策略,支持离线和堆外缓存 | 单应用,需要快速访问且数据量不是分布式共享的场景 |
Caffeine | 本地缓存 | 应用逻辑维护 | 非常高 | 高 | 基于权重的驱逐策略,高性能 | 高性能要求的场景,如高频读写的小数据集 |
Spring Cache | 缓存抽象层 | 依赖底层实现 | 依赖底层实现 | 高 | 缓存技术无关的编程模型,易于切换底层缓存实现 | 需要灵活切换或结合多种缓存技术的场景 |
Redis | 分布式缓存 | 高 | 中至高 | 中 | 支持多种数据结构,持久化,主从复制和哨兵系统 | 需要数据共享,高并发,分布式场景 |
J2Cache | 结合本地与分布式 | 较高 | 高 | 中至高 | 一级缓存与二级缓存结合,支持Redis、Ehcache等多种二级缓存后端 | 需要结合本地快速访问和分布式数据共享的场景 |
Memcached | 分布式缓存 | 中 | 高 | 高 | 简单的键值存储,不支持持久化 | 高速缓存临时数据,如会话存储,不需要持久化的场景 |
Guava Cache | 本地缓存 | 应用逻辑维护 | 高 | 高 | 易于使用的本地缓存,支持多种缓存过期策略 | 适用于单个应用内需要快速访问的数据缓存,不需分布式数据共享的场景 |
3、为什么选择J2Cache
为什么选择J2Cache? 在众多缓存框架中,J2Cache 脱颖而出,不仅因为其解决了常见的缓存问题,更因为它在设计理念上与众不同。常见的缓存解决方案大体分为两类:内存缓存(如 Ehcache),它速度快,适用于进程内;集中式缓存(如 Redis),能够服务于多节点。尽管市场上的缓存框架已经非常成熟,但J2Cache 并不简单地加入竞争,而是旨在解决以下关键问题:
- 使用内存缓存时,应用重启后缓存数据丢失,可能会导致缓存雪崩,对数据库造成巨大压力,引发应用阻塞。
- 在内存缓存模式下,多个应用节点无法共享缓存数据。
- 集中式缓存(如Redis)面对大量数据读取时,尽管服务负载不高,但可能因网络带宽达到极限而使数据读取速度大幅下降。
J2Cache 应对这些挑战的策略是创新的双层缓存模式:利用成熟的内存缓存框架作为一级缓存,Redis 作为二级缓存。通过这种结构,所有数据首先尝试从一级缓存获取,如果不存在,再从二级缓存中读取。这大幅减少了对二级缓存的访问次数,极大地降低了Redis服务器的数据流量压力。
此外,J2Cache 通过 Redis Pub/Sub 和 JGroups 提供了节点间数据同步解决方案,确保了缓存数据的一致性。当缓存数据需要更新时,J2Cache 会通知集群内的其他节点,使它们可以清除自己的内存数据并重新从Redis读取最新数据。
对于担心应用节点内存占用飙升的人来说,J2Cache 的设计考虑到了服务器内存的充足性,同时提供了配置选项以控制内存中存储的数据量,避免内存溢出问题。
4、J2Cache介绍
J2Cache 是由开源社区 OSChina 开发并广泛应用的一个高性能双层缓存框架,专为解决企业级Java应用中的缓存问题而设计。它兼容Java 8及以上版本,提供了一种高效的缓存解决方案,尤其适用于需要高可用性和扩展性的集群环境。通过采用两级缓存策略,J2Cache旨在优化数据访问速度,降低对后端存储系统的访问压力,从而提升整体应用性能。
4.1两级缓存架构
第一级(L1)缓存: 位于应用进程内,使用内存缓存技术(支持Ehcache 2.x、Ehcache 3.x和Caffeine等主流内存缓存实现)。L1缓存提供极低延迟的数据访问,用于快速响应应用请求,减少数据访问的延迟。
第二级(L2)缓存: 采用外部缓存存储,如Redis(推荐)或Memcached,适用于分布式环境中的数据共享和持久化。L2缓存作为L1缓存的补充,用于处理更大规模的数据存储需求,保证数据的一致性和可靠性。
4.2缓存协作机制
在J2Cache的架构中,两级缓存协同工作,以实现数据的高效访问和同步。当应用请求数据时,先在L1缓存中查找;如果L1缓存未命中,则查询L2缓存。在L2缓存找到数据后,该数据将被回填到L1缓存,以便未来的访问可以直接从L1缓存中获得,大幅减少数据访问时间。此外,J2Cache提供了灵活的缓存过期和数据同步策略,确保了缓存数据的实时性和一致性。
4.3应用场景与优势
J2Cache 非常适合部署在需要处理高并发请求和大数据量的企业级应用中。在集群环境下,J2Cache可以显著降低对后端数据库的压力,提高数据处理速度,优化资源利用率,从而提升整体应用性能和用户体验。即使在单机环境中,J2Cache也能有效避免应用重启导致的缓存冷启动问题,减少对后端业务的冲击。
5、J2Cache 组播
在深入了解J2Cache的核心工作原理之前,我们已经知道J2Cache是一个强大的双层缓存框架,通过内存缓存和外部缓存(如Redis或Memcached)的结合使用,提升应用性能和数据访问速度。然而,缓存同步机制是保持集群环境中数据一致性的关键技术,J2Cache为此提供了两种主要的同步策略,确保在分布式环境中缓存数据的一致性和实时性。
5.1缓存同步策略
1. Redis Pub/Sub机制(不推荐): 这一策略基于Redis的发布/订阅功能实现缓存之间的数据同步。尽管这种方法在某些场景下可以工作,但由于它可能带来的潜在问题(例如,消息的可靠性和网络带宽的使用效率),这种同步方式并不是最优选。
2. JGroups广播机制(推荐): J2Cache推荐使用JGroups的广播机制进行缓存同步,特别是在配置为TCP模式时。JGroups提供了一个可靠的群组通信工具,通过广播消息到所有集群节点来同步缓存数据。这种方式不仅提高了同步的可靠性,而且在多种网络环境下都表现出较高的效率和稳定性。
5.2原理介绍
无论采用哪种同步策略,J2Cache的目标都是确保所有集群节点中的缓存数据保持同步。当缓存数据在一个节点上更新时,该变更会通过选定的同步机制广播到所有其他节点。收到同步消息的节点会相应地更新或清除其本地缓存中的数据,以此来保证数据的一致性。
JGroups的TCP模式特别适用于需要高度可靠性和跨多种网络配置的环境,因为它能有效地管理网络中的数据同步和通信。这种方式减少了因缓存不一致导致的数据错误和应用逻辑失败的风险,是在构建分布式应用和服务时保持数据同步的可靠方法。
J2Cache通过提供灵活的缓存同步策略,支持在复杂的分布式环境中高效、可靠地同步缓存数据。其推荐的JGroups广播机制,尤其是TCP模式,为应用提供了一种高效、稳定且易于管理的缓存同步解决方案,确保了应用性能的最优化和数据一致性的维护。
6、使用方法及实际示例
以下实际的使用示例是以Springboot 项目为基础集成 J2Cache 的。
6.1引用 Maven
以下是我从中央仓库查看的2.8.x的版本
xml
<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-core</artifactId>
<version>2.8.2-release</version>
</dependency>
3.2 准备配置
为基于Spring框架的Java应用程序设置缓存策略和参数,涉及到使用Redis和Caffeine作为缓存提供者。它配置了应用程序运行的端口为9000,并设置了Redis作为二级缓存的存储方式,其中包括Redis的连接信息(如主机IP、端口6379、使用的数据库索引0等)。此外,还明确了使用Lettuce作为Redis客户端。
在缓存策略方面,选用了被动清除模式(passive
),在此模式下,一级缓存过期后会通知其他节点清除一级和二级缓存。同时,开启了二级缓存,并使用了caffeine作为一级缓存提供者,Redis作为二级缓存提供者。配置还涉及到了缓存的序列化方式,以及与Lettuce客户端相关的一些参数设置,比如连接模式、最大连接数、空闲连接数等。
j2cache.properties
yaml
server:
port: 9000
spring:
redis:
host: IP
password:
port: 6379
database: 0
j2cache:
# 缓存清除模式
# active:主动清除,二级缓存过期主动通知各节点清除,优点在于所有节点可以同时收到缓存清除
# passive:被动清除,一级缓存过期进行通知各节点清除一二级缓存
# blend:两种模式一起运作,对于各个节点缓存准确性以及及时性要求高的可以使用(推荐使用前面两种模式中一种)
cache-clean-mode: passive
allow-null-values: true
redis-client: lettuce #指定redis客户端使用lettuce,也可以使用Jedis
l2-cache-open: true #开启二级缓存
# 使用springRedis进行广播通知缓失效
broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
L1: #指定一级缓存提供者为caffeine
provider_class: caffeine
L2: #使用springRedis替换二级缓存 指定二级缓存提供者为redis
provider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProvider
# 如果要使用lettuce客户端请配置为lettuce
config_section: lettuce
sync_ttl_to_redis: true
default_cache_null_object: false
serialization: fst #序列化方式:fst、kyro、Java 区别:存储字节大小
caffeine:
properties: /caffeine.properties
lettuce:
mode: single
namespace:
storage: generic
channel: j2cache
scheme: redis
hosts: IP:6379
password:
database: 0
sentinelMasterId:
maxTotal: 100
maxIdle: 10
minIdle: 10
timeout: 10000
使用你喜欢的文本编辑器打开 j2cache.yml
并找到 redis.hosts
项,将其信息改成你的 Redis 服务器所在的地址和端口。
我们建议缓存在使用之前都需要预先设定好缓存大小及有效时间,使用文本编辑器打开 caffeine.properties 进行缓存配置,配置方法请参考文件中的注释内容。
例如:default = 1000,30m #定义缓存名 default ,对象大小 1000,缓存数据有效时间 30 分钟。 你可以定义多个不同名称的缓存。
自己代码中实际的配置文件已上传至资源中心,方便查看:
6.3添加配置
指定不同缓存区域的缓存个数和过期时间。格式遵循[名称]=缓存个数,过期时间
的结构,其中过期时间支持秒(s)、分钟(m)、小时(h)和天(d)为单位。
default=2000, 2h
:这条规则为名为"default"的缓存区域设置了最多2000个缓存项,每个缓存项的过期时间为2小时。这意味着如果缓存项存储后超过2小时没有被访问,则会被自动清除。web=50, 2h
:这条规则为名为"web"的缓存区域设置了最多50个缓存项,每个缓存项的过期时间同样为2小时。这是针对特定用途或分类的缓存策略,可能用于存储Web层面的数据。
ini
名称=缓存个数,过期时间
# [name] = size, xxxx[s|m|h|d]
default=2000, 2h
web=50, 2h
允许应用根据不同的需求和资源类型设置不同的缓存大小和过期策略,优化资源使用和性能。
6.4使用实例
kotlin
@RestController
@RequestMapping("/cache")
public class CacheController {
private final String key = "myKey";
private String region = "web";
@Autowired
private CacheChannel cacheChannel;
/**
* 设置缓存或从缓存取值
*
* @return
*/
@GetMapping("/getData")
public List<String> getInfos() {
CacheObject cacheObject = cacheChannel.get(region, key);
if (cacheObject.getValue() == null) {
//缓存中没有找到,查询数据库获得
List<String> data = new ArrayList<String>();
data.add("info1");
data.add("info2");
//放入缓存
cacheChannel.set(region, key, data);
return data;
}
return (List<String>) cacheObject.getValue();
}
/**
* 清理指定缓存
*
* @return
*/
@GetMapping("/evict")
public String evict() {
cacheChannel.evict(region, key);
return "evict success";
}
/**
* 检测存在那级缓存
*
* @return
*/
@GetMapping("/check")
public String check() {
int check = cacheChannel.check(region, key);
return "level:" + check;
}
/**
* 检测缓存数据是否存在
*
* @return
*/
@GetMapping("/exists")
public String exists() {
boolean exists = cacheChannel.exists(region, key);
return "exists:" + exists;
}
/**
* 清理指定区域的缓存
*
* @return
*/
@GetMapping("/clear")
public String clear() {
cacheChannel.clear(region);
return "clear success";
}
}
7、结语
在系统中实施j2cache缓存框架时,数据更新是如何在一级(内存)和二级(Redis)缓存之间协同工作并保持一致的。这一过程不仅提升了数据处理的效率,也避免了可能由于缓存数据不一致而导致的问题,确保了系统的高性能和稳定性。J2Cache的实施经验不仅为我们的系统带来了显著的性能提升,也为面对未来可能的挑战提供了宝贵的经验和信心。