我们的微服务架构中,存在一个单独的基础数据中心,存放了各个服务、页面、app端的所需要的基础数据信息。这些数据的特点就是不易变,查询量大;最适合的场景就是进行缓存。经过一番商讨,决定使用J2Cache二级缓存。

 

整个缓存架构过程:

 

具体更多关于J2cache可以去查看官网文档

 

我这里简述我们的使用方法,因为我们是springboot项目,Spring的IOC可以让我们将所有单例组件都交给他来管理。

 

J2cache原生使用的是静态类启动,使用。这既涉及到一个问题我们项目区分了4套环境,都是使用spring的profile来控制的。

所以我们的缓存框架就需要将多环境做进去。这里我就将他改造成了@component组件。默认L1缓存使用caffeine。L2使用redis

首先将J2cache的配置文件抽离出来,以configurationProperties的形式进行管理。

package org.codingfly.common.cache;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * j2cache配置项
 *
 * @author: wangmeng
 * @date: 2019/12/26
 **/
@ConfigurationProperties("codingfly.j2cache")
@Component
@Getter
@Setter
public class J2cacheProperties {

    private boolean enable = true;

    private String broadcast = "lettuce";

    private L1cacheDefinition l1cache = new L1cacheDefinition();

    private L2cacheDefinition l2cache = new L2cacheDefinition();

    private String serialization = "json";

    private boolean syncTtlToRedis = true;

    private boolean cacheNullObject = true;

    private List<CaffeineDefinition> caffeine = new ArrayList<>();

}
package org.codingfly.common.cache;

import lombok.Getter;
import lombok.Setter;

/**
 * 一级缓存定义
 *
 * @author: wangmeng
 * @date: 2019/12/26
 **/
@Getter
@Setter
class L1cacheDefinition {

    private String name = "caffeine";
}
package org.codingfly.common.cache;

import lombok.Getter;
import lombok.Setter;

/**
 * 二级缓存定义
 *
 * @author: wangmeng
 * @date: 2019/12/26
 **/
@Getter
@Setter
class L2cacheDefinition {

    private String name = "lettuce";

}

 

然后我们使用@conditionsProperties注解配置cacheConfig类

 

package org.codingfly.common.cache;

import net.oschina.j2cache.J2CacheConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * J2CacheConfig配置
 *
 * @author: wangmeng
 * @date:
 **/
@Configuration
@ConditionalOnProperty(prefix = "codingfly.j2cache", name = "enable", havingValue = "true", matchIfMissing = true)
public class CacheConfig {

    private final J2cacheProperties j2cacheProperties;

    private final RedisProperties redisProperties;

    public CacheConfig(J2cacheProperties j2cacheProperties, RedisProperties redisProperties) {
        this.j2cacheProperties = j2cacheProperties;
        this.redisProperties = redisProperties;
    }

    @Bean
    public J2CacheConfig j2CacheConfig() {
        J2CacheConfig j2CacheConfig = new J2CacheConfig();
        j2CacheConfig.setBroadcast(j2cacheProperties.getBroadcast());
        j2CacheConfig.setL1CacheName(j2cacheProperties.getL1cache().getName());
        j2CacheConfig.setL2CacheName(j2cacheProperties.getL2cache().getName());
        j2CacheConfig.setSerialization(j2cacheProperties.getSerialization());
        j2CacheConfig.setSyncTtlToRedis(j2cacheProperties.isSyncTtlToRedis());
        j2CacheConfig.setDefaultCacheNullObject(j2cacheProperties.isCacheNullObject());
        j2CacheConfig.setBroadcastProperties(getBroadcastProperties());
        j2CacheConfig.setL1CacheProperties(getL1CacheProperties());
        j2CacheConfig.setL2CacheProperties(getBroadcastProperties());
        return j2CacheConfig;
    }

    private Properties getBroadcastProperties() {
        Properties broadcastProperties = new Properties();
        broadcastProperties.setProperty("namespace", "");
        //storage的另一个配置broadcastProperties.setProperty("storage", "hash")
        broadcastProperties.setProperty("storage", "generic");
        broadcastProperties.setProperty("channel", "j2cache");
        broadcastProperties.setProperty("scheme", "redis");
        broadcastProperties.setProperty("hosts", redisProperties.getHost() + ":" + redisProperties.getPort());
        broadcastProperties.setProperty("password", "");
        broadcastProperties.setProperty("database", String.valueOf(redisProperties.getDatabase()));
        broadcastProperties.setProperty("sentinelMasterId", "");
        broadcastProperties.setProperty("maxTotal", "100");
        broadcastProperties.setProperty("maxIdle", "10");
        broadcastProperties.setProperty("minIdle", "10");
        broadcastProperties.setProperty("timeout", "1000");
        return broadcastProperties;
    }

    private Properties getL1CacheProperties() {
        Properties l1CacheProperties = new Properties();
        l1CacheProperties.setProperty("region.default", "1000, 30m");
        j2cacheProperties.getCaffeine().forEach(data ->
                l1CacheProperties.setProperty("region." + data.getKey(), data.getValue()));
        return l1CacheProperties;
    }
}

这时候我们就已经将J2cache需要的J2cacheconfig类注入到spring管理了。

 

package org.codingfly.common.cache;

import net.oschina.j2cache.CacheChannel;
import net.oschina.j2cache.J2CacheBuilder;
import net.oschina.j2cache.J2CacheConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * channel配置
 *
 * @author: wangmeng
 * @date: 2019/12/25
 **/
@Configuration
@ConditionalOnBean(CacheConfig.class)
public class CacheChannelConfig {

    private final J2CacheConfig j2CacheConfig;

    public CacheChannelConfig(J2CacheConfig j2CacheConfig) {
        this.j2CacheConfig = j2CacheConfig;
    }

    @Bean
    public CacheChannel cacheChannel() {
        return J2CacheBuilder.init(j2CacheConfig).getChannel();
    }

}

接下来我们需要配置我们的cachechannel类注入。

 

这时候我们的完成了注入cacheChannel组件。这里我们注入时因为技术选型已经选好了,所以配置都使用了默认值。即使不配置自定义配置也可以使用。只要配置了redis即可。默认的redis连接使用lettuce。

默认还增加了caffeine的default配置。其他的缓存配置,默认会遵照此配置。

 

我们还可以将它和Spring cache注解集成,这里是官网提供的方法 对其进行了改造。

 

package org.codingfly.common.cache;

import net.oschina.j2cache.CacheChannel;
import net.oschina.j2cache.NullObject;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.AbstractValueAdaptingCache;

import java.util.concurrent.Callable;

/**
 * Spring Cache Adapter
 *
 * @author wangmeng
 * @date: 2019/12/27
 */
public class J2CacheSpringCacheAdapter extends AbstractValueAdaptingCache {
    private final String name;
    private final CacheChannel j2Cache;
    private boolean allowNullValues;

    /**
     * Create an {@code AbstractValueAdaptingCache} with the given setting.
     *
     * @param allowNullValues whether to allow for {@code null} values
     * @param j2Cache         j2cache instance
     * @param name            cache region name
     */
    J2CacheSpringCacheAdapter(boolean allowNullValues, CacheChannel j2Cache, String name) {
        super(allowNullValues);
        this.allowNullValues = allowNullValues;
        this.name = name;
        this.j2Cache = j2Cache;
    }

    /**
     * Perform an actual lookup in the underlying store.
     *
     * @param key the key whose associated value is to be returned
     * @return the raw store value for the key
     */

    private static String getKey(Object key) {
        return key.toString();
    }

    @Override
    protected Object lookup(Object key) {
        Object value = j2Cache.get(name, getKey(key)).rawValue();
        if (value == null || value.getClass().equals(Object.class)) {
            return null;
        }
        return value;
    }

    /**
     * Return the cache name.
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Return the underlying native cache provider.
     */
    @Override
    public Object getNativeCache() {
        return j2Cache;
    }

    /**
     * Return the value to which this cache maps the specified key, obtaining
     * that value from {@code valueLoader} if necessary. This method provides
     * a simple substitute for the conventional "if cached, return; otherwise
     * create, cache and return" pattern.
     * <p>If possible, implementations should ensure that the loading operation
     * is synchronized so that the specified {@code valueLoader} is only called
     * once in case of concurrent access on the same key.
     * <p>If the {@code valueLoader} throws an exception, it is wrapped in
     * a {@link ValueRetrievalException}
     *
     * @param key         the key whose associated value is to be returned
     * @param valueLoader value loader
     * @return the value to which this cache maps the specified key
     * @throws ValueRetrievalException if the {@code valueLoader} throws an exception
     * @since 4.3
     */
    @Override
    public <T> T get(Object key, Callable<T> valueLoader) throws ValueRetrievalException {
        ValueWrapper val = get(key);
        if (val != null) {
            if (val.get() instanceof NullObject) {
                return null;
            }
            return (T) val.get();
        }
        T t;
        try {
            t = valueLoader.call();
        } catch (Exception e) {
            throw new ValueRetrievalException(key, valueLoader, e);
        }
        this.put(key, t);
        return t;
    }

    /**
     * Associate the specified value with the specified key in this cache.
     * <p>If the cache previously contained a mapping for this key, the old
     * value is replaced by the specified value.
     *
     * @param key   the key with which the specified value is to be associated
     * @param value the value to be associated with the specified key
     */
    @Override
    public void put(Object key, Object value) {
        j2Cache.set(name, getKey(key), value, allowNullValues);
    }

    /**
     * Atomically associate the specified value with the specified key in this cache
     * if it is not set already.
     * <p>This is equivalent to:
     * <pre><code>
     * Object existingValue = cache.get(key);
     * if (existingValue == null) {
     *     cache.put(key, value);
     *     return null;
     * } else {
     *     return existingValue;
     * }
     * </code></pre>
     * except that the action is performed atomically. While all out-of-the-box
     * {@link CacheManager} implementations are able to perform the put atomically,
     * the operation may also be implemented in two steps, e.g. with a check for
     * presence and a subsequent put, in a non-atomic way. Check the documentation
     * of the native cache implementation that you are using for more details.
     *
     * @param key   the key with which the specified value is to be associated
     * @param value the value to be associated with the specified key
     * @return the value to which this cache maps the specified key (which may be
     * {@code null} itself), or also {@code null} if the cache did not contain any
     * mapping for that key prior to this call. Returning {@code null} is therefore
     * an indicator that the given {@code value} has been associated with the key.
     * @since 4.1
     */
    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        ValueWrapper existingValue = get(getKey(key));
        if (existingValue == null) {
            put(key, value);
            return null;
        } else {
            return existingValue;
        }

    }

    /**
     * Evict the mapping for this key from this cache if it is present.
     *
     * @param key the key whose mapping is to be removed from the cache
     */
    @Override
    public void evict(Object key) {
        j2Cache.evict(name, getKey(key));
    }

    /**
     * Remove all mappings from the cache.
     */
    @Override
    public void clear() {
        j2Cache.clear(name);
    }
}
package org.codingfly.common.cache;

import net.oschina.j2cache.CacheChannel;
import org.springframework.cache.Cache;
import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager;

import java.util.Collection;

/**
 * @author Chen
 */
public class J2CacheSpringCacheManageAdapter extends AbstractTransactionSupportingCacheManager {
    /**
     * Load the initial caches for this cache manager.
     * <p>Called by {@link #afterPropertiesSet()} on startup.
     * The returned collection may be empty but must not be {@code null}.
     */
    @Override
    protected Collection<? extends Cache> loadCaches() {
        return null;
    }

    private CacheChannel cacheChannel;

    private boolean allowNullValues;

    /**
     * @param allowNullValues 默认 true
     */
    J2CacheSpringCacheManageAdapter(CacheChannel cacheChannel, boolean allowNullValues) {
        this.cacheChannel = cacheChannel;
        this.allowNullValues = allowNullValues;
    }

    @Override
    protected Cache getMissingCache(String name) {
        return new J2CacheSpringCacheAdapter(allowNullValues, cacheChannel, name);
    }
}
package org.codingfly.common.cache;

import net.oschina.j2cache.CacheChannel;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

/**
 * j2cache集成spring cache注解使用
 *
 * @author: wangmeng
 * @date: 2019/12/26
 **/
@Configuration
@ConditionalOnBean(CacheChannel.class)
@EnableCaching
public class SpringCacheConfig extends CachingConfigurerSupport {

    private final CacheChannel cacheChannel;

    public SpringCacheConfig(CacheChannel cacheChannel) {
        this.cacheChannel = cacheChannel;
    }

    @Override
    public CacheManager cacheManager() {
        return new J2CacheSpringCacheManageAdapter(cacheChannel, true);
    }

}

这时我们已经将它集成到了Spring cache的注解中,这里嗨解决了一个spring cache'使用redis做缓存时,不可以设置超时时间的一个问题。

 

使用方法同spring cache。

到底spring boot 集成J2cache 的两种方法都写完了。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐