j2CaChe在项目中的应用(二级缓存)

3 篇文章 0 订阅

j2CaChe项目地址

不少人看到 J2Cache 第一眼时,会认为这就是一个普普通通的缓存框架,和例如 Ehcache、Caffeine 、Spring Cache 之类的项目没什么区别,无非是造了一个新的轮子而已。事实上完全不是一回事!

目前缓存的解决方案一般有两种:

内存缓存(如 Ehcache) —— 速度快,进程内可用
集中式缓存(如 Redis)—— 可同时为多节点提供服务
现有的缓存框架已经非常成熟而且优秀,J2Cache 无心造一个新的轮子,它要解决的几个问题如下:

使用内存缓存时,一旦应用重启后,由于缓存数据丢失,缓存雪崩,给数据库造成巨大压力,导致应用堵塞
使用内存缓存时,多个应用节点无法共享缓存数据
使用集中式缓存,由于大量的数据通过缓存获取,导致缓存服务的数据吞吐量太大,带宽跑满。现象就是 Redis 服务负载不高,但是由于机器网卡带宽跑满,导致数据读取非常慢
在遭遇问题1、2 时,很多人自然而然会想到使用 Redis 来缓存数据,因此就难以避免的导致了问题3的发生。

当发生问题 3 时,又有很多人想到 Redis 的集群,通过集群来降低缓存服务的压力,特别是带宽压力。

但其实,这个时候的 Redis 上的数据量并不一定大,仅仅是数据的吞吐量大而已。

咱们假设这样一个场景:

有这么一个网站,某个页面每天的访问量是 1000万,每个页面从缓存读取的数据是 50K。缓存数据存放在一个 Redis 服务,机器使用千兆网卡。那么这个 Redis 一天要承受 500G 的数据流,相当于平均每秒钟是 5.78M 的数据。而网站一般都会有高峰期和低峰期,两个时间流量的差异可能是百倍以上。我们假设高峰期每秒要承受的流量比平均值高 50 倍,也就是说高峰期 Redis 服务每秒要传输超过 250 兆的数据。请注意这个 250 兆的单位是 byte,而千兆网卡的单位是“bit” ,你懂了吗? 这已经远远超过 Redis 服务的网卡带宽。

所以如果你能发现这样的问题,一般你会这么做:

升级到万兆网卡 —— 这个有多麻烦,相信很多人知道,特别是一些云主机根本没有万兆网卡给你使用(有些运维工程师会给这样的建议)
多个 Redis 搭建集群,将流量分摊多多台机器上
如果你采用第2种方法来解决上述的场景中碰到的问题,那么你最好准备 5 个 Redis 服务来支撑。在缓存服务这块成本直接攀升了 5 倍。你有钱当然没任何问题,但是结构就变得非常复杂了,而且可能你缓存的数据量其实不大,1000 万高频次的缓存读写 Redis 也能轻松应付,可是因为带宽的问题,你不得不付出 5 倍的成本。

那么 J2Cache 的用武之处就在这里。
如果我们不用每次页面访问的时候都去 Redis 读取数据,那么 Redis 上的数据流量至少降低 1000 倍甚至更多,以至于一台 Redis 可以轻松应付。

J2Cache 其实不是一个缓存框架,它是一个缓存框架的桥梁。它利用现有优秀的内存缓存框架作为一级缓存,而把 Redis 作为二级缓存。所有数据的读取先从一级缓存中读取,不存在时再从二级缓存读取,这样来确保对二级缓存 Redis 的访问次数降到最低。

有人会质疑说,那岂不是应用节点的内存占用要飙升?我的答案是 —— 现在服务器的内存都是几十 G 打底,多则百 G 数百 G,这点点的内存消耗完全不在话下。其次一级缓存框架可以通过配置来控制在内存中存储的数据量,所以不用担心内存溢出的问题。

剩下的另外一个问题就是,当缓存数据更新的时候,怎么确保每个节点内存中的数据是一致的。而这一点算你问到点子上了,这恰恰是 J2Cache 的核心所在。

J2Cache 目前提供两种节点间数据同步的方案 —— Redis Pub/Sub 和 JGroups 。当某个节点的缓存数据需要更新时,J2Cache 会通过 Redis 的消息订阅机制或者是 JGroups 的组播来通知集群内其他节点。当其他节点收到缓存数据更新的通知时,它会清掉自己内存里的数据,然后重新从 Redis 中读取最新数据。

这就完成了 J2Cache 缓存数据读写的闭环。

为什么不用 Ehcache 的集群方案?

对 Ehcache 比较熟悉的人还会问的就是这个问题,Ehcache 本身是提供集群模式的,可以在多个节点同步缓存数据。但是 Ehcache 的做法是将整个缓存数据在节点间进行传输。如咱们前面的说的,一个页面需要读取 50K 的缓存数据,当这 50K 的缓存数据有更新时,那么需要在几个节点间传递整个 50K 的数据。这也会造成应用节点间大量的数据传输,这个情况完全不可控。

补充:当然这个单个数据传输量本身并不比使用 J2Cache 多,但是 ehcache 利用 jgroups 来同步数据的做法,在实际测试过程中发现可靠性还是略低,而且 jgroups 的同步数据在云主机上无法使用。

而 J2Cache 传输的仅仅是缓存的 key 而已,因此相比 Ehcache 的集群模式,J2Cache 要传输的数据极其小,对节点间的数据通信完全不会产生大的影响。

j2CaChe实战

maven加入j2CaChe 依赖包

        <dependency>
            <groupId>net.oschina.j2cache</groupId>
            <artifactId>j2cache-spring-boot2-starter</artifactId>
            <version>2.7.6-release</version>
        </dependency>

        <dependency>
            <groupId>net.oschina.j2cache</groupId>
            <artifactId>j2cache-core</artifactId>
            <version>2.7.8-release</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba</groupId>
                    <artifactId>fastjson</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

下图是注意的要配置的图示

在这里插入图片描述

1 application.yml

在这里插入图片描述

2 application-dev.yml

在这里插入图片描述

3 j2cache-dev.properties

#J2Cache configuration

#########################################
# Cache Broadcast Method
# values:
# jgroups -> use jgroups's multicast
# redis -> use redis publish/subscribe mechanism (using jedis)
# lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
# rabbitmq -> use RabbitMQ publisher/consumer mechanism
# rocketmq -> use RocketMQ publisher/consumer mechanism
# none -> don't notify the other nodes in cluster
# xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
#########################################
# 是否启用广播,如果只是单节点就不需要
j2cache.broadcast = none

# jgroups properties
#jgroups.channel.name = j2cache
#jgroups.configXml = /network.xml

# RabbitMQ properties
rabbitmq.exchange = j2cache
rabbitmq.host = 47.101.190.XX
rabbitmq.port = 5672
rabbitmq.username = admin
rabbitmq.password = root

# RocketMQ properties
#rocketmq.name = j2cache
#rocketmq.topic = j2cache
# use ; to split multi hosts
#rocketmq.hosts = 127.0.0.1:9876

#########################################
# Level 1&2 provider
# values:
# none -> disable this level cache
# ehcache -> use ehcache2 as level 1 cache
# ehcache3 -> use ehcache3 as level 1 cache
# caffeine -> use caffeine as level 1 cache(only in memory)
# redis -> use redis as level 2 cache (using jedis)
# lettuce -> use redis as level 2 cache (using lettuce)
# readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
# memcached -> use memcached as level 2 cache (xmemcached),
# [classname] -> use custom provider
#########################################

j2cache.L1.provider_class = caffeine
j2cache.L2.provider_class = lettuce

# When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
# j2cache.L2.config_section = redis

# Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
# NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
j2cache.sync_ttl_to_redis = true

# Whether to cache null objects by default (default false)
j2cache.default_cache_null_object = false

#########################################
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kyro -> using kyro serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# [classname implements Serializer]
#########################################

j2cache.serialization = fastjson
#json.map.person = net.oschina.j2cache.demo.Person

#########################################
# Ehcache configuration
#########################################

# ehcache.configXml = /ehcache.xml

# ehcache3.configXml = /ehcache3.xml
# ehcache3.defaultHeapSize = 1000

#########################################
# Caffeine configuration
# caffeine.region.[name] = size, xxxx[s|m|h|d]
#
#########################################
caffeine.properties = /j2cache-caffeine.properties

#########################################
# Lettuce scheme
#
# redis -> single redis server
# rediss -> single redis server with ssl
# redis-sentinel -> redis sentinel
# redis-cluster -> cluster servers
#
#########################################

lettuce.namespace =
lettuce.mode = single
lettuce.cluster_name = j2cache
lettuce.storage = generic
lettuce.channel = j2cache
lettuce.channel.host =
lettuce.scheme = redis
lettuce.hosts = 47.101.190.XX:6379
lettuce.password = root
lettuce.database = 10
lettuce.sentinelMasterId =
lettuce.maxTotal = 100
lettuce.maxIdle = 10
lettuce.numTestsPerEvictionRun = 10
lettuce.softMinEvictableIdleTimeMillis = 10
lettuce.minIdle = 10
lettuce.maxWaitMillis = 5000
lettuce.lifo = false
lettuce.minEvictableIdleTimeMillis = 60000
# timeout in milliseconds
lettuce.timeout = 10000
lettuce.testOnBorrow = true
lettuce.testOnReturn = false
lettuce.testWhileIdle = true
lettuce.timeBetweenEvictionRunsMillis = 300000
lettuce.blockWhenExhausted = false


#########################################
# Redis connection configuration
#########################################

#########################################
# Redis Cluster Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers  (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0)
#
#########################################

#redis.mode = single

#redis storage mode (generic|hash)
#redis.storage = generic

## redis pub/sub channel name
#redis.channel = j2cache
## redis pub/sub server (using redis.hosts when empty)
#redis.channel.host =

#cluster name just for sharded
#redis.cluster_name = j2cache

## redis cache namespace optional, default[empty]
#redis.namespace =

## connection
# Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379

#redis.hosts = 47.101.156.198:6379
#redis.timeout = 2000
#redis.password = Xykjtest2018
#redis.database = 3

## redis pool properties
#redis.maxTotal = 100
#redis.maxIdle = 10
#redis.maxWaitMillis = 5000
#redis.minEvictableIdleTimeMillis = 60000
#redis.minIdle = 1
#redis.numTestsPerEvictionRun = 10
#redis.lifo = false
#redis.softMinEvictableIdleTimeMillis = 10
#redis.testOnBorrow = true
#redis.testOnReturn = false
#redis.testWhileIdle = true
#redis.timeBetweenEvictionRunsMillis = 300000
#redis.blockWhenExhausted = false
#redis.jmxEnabled = false


#########################################
# memcached server configurations
# refer to https://gitee.com/mirrors/XMemcached
#########################################

#memcached.servers = 127.0.0.1:11211
#memcached.username =
#memcached.password =
#memcached.connectionPoolSize = 10
#memcached.connectTimeout = 1000
#memcached.failureMode = false
#memcached.healSessionInterval = 1000
#memcached.maxQueuedNoReplyOperations = 100
#memcached.opTimeout = 100
#memcached.sanitizeKeys = false

j2cache-caffeine.properties

#########################################
# Caffeine configuration
# [name] = size, xxxx[s|m|h|d]
#########################################
default=1000, 30m
# 用户基础信息缓存
userInfoCH=10000, 30m
# 登录用户缓存
userContextCH=10000, 30m
# 门店缓存
storeCH=10000, 30m

在这里插入图片描述
CacheConfigsEnum.java

package com.xy.pay.main.cache.impl;

import com.xy.common.verify.Asserts;

import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * 系统缓存名 , 注意这里配置完后,还要配置 caffeine.properties . 并以  caffeine.properties 为主
 *
 */
public enum CacheConfigsEnum {

    /**
     * 登录用户缓存
     */
    USER_CONTEXT_CACHE("userContextCH"),

    /***
     * 用户基础信息缓存
     */
    USER_INFO_CACHE("userInfoCH"),

    /**
     * 门店缓存
     */
    STORE_CACHE("storeCH"),

    ;

    private final String cacheName;       //缓存名


    static {
        //验证【value】必须是唯一的
        int size = Arrays.stream(CacheConfigsEnum.values())
                .map(CacheConfigsEnum::getCacheName)
                .collect(Collectors.toSet())
                .size();
        Asserts.state(size == CacheConfigsEnum.values().length, "CacheConfigsEnum.cacheName 重复定义");
    }

    CacheConfigsEnum(String cacheName) {
        this.cacheName = cacheName;
    }

    /**
     * 获取缓存名
     *
     * @return
     */
    public String getCacheName() {
        return cacheName;
    }


}

StoreCacheImpl.java

package com.xy.pay.main.cache.impl;

import com.xy.common.verify.Asserts;
import com.xy.pay.main.cache.StoreCache;
import com.xy.pay.main.dao.model.Store;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 */
@Component
public class StoreCacheImpl implements StoreCache {

    private final Cache cache;

    @Autowired
    public StoreCacheImpl(CacheManager cacheManager) {
        cache = cacheManager.getCache(CacheConfigsEnum.STORE_CACHE.getCacheName());
        Asserts.notNull(cache);
    }

    /**
     * 根据门店id 获取
     *
     * @param storeId
     */
    @Override
    public Optional<Store> getById(Long storeId) {
        Asserts.notNull(storeId);
        Store store = this.cache.get(storeId, Store.class);
        return Optional.ofNullable(store);
    }

    /**
     * 添加缓存
     *
     * @param store
     */
    @Override
    public void addStore(Store store) {
        Asserts.notNull(store);
        Asserts.notNull(store.getId());
        this.cache.put(store.getId(), store);
    }

    /**
     * 根据id清空缓存
     *
     * @param storeId
     */
    @Override
    public void clearStore(Long storeId) {
        Asserts.notNull(storeId);
        this.cache.evict(storeId);
    }


}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值