Spring配置cache(concurrentHashMap,guava cache、redis实现)附源码

  在应用程序中,数据通常是存在数据库中(磁盘介质),对于某些被频繁访问的数据,若是每次都访问数据库,不只涉及到网络io,还受到数据库查询的影响;而目前一般会将频繁使用,而且不常常改变的数据放入缓存中,从缓存中查询数据的效率要高于数据库,由于缓存通常KV形式存储,而且是将数据存在“内存”中,从内存访问数据是至关快的。html

  对于频繁访问,须要缓存的数据,咱们通常是这样作的:java

  一、当收到查询请求,先去查询缓存,若是缓存中查询到数据,那么直接将查到的数据做为响应数据;node

  二、若是缓存中没有找到要查询的数据,那么就从其余地方,好比数据库中查询出来,若是从数据库中查到了数据,就将数据放入缓存后,再将数据返回,下一次能够直接从缓存查询;git

  这里就不进一步探究“缓存穿透”的问题,有兴趣能够本身学习一下。github

  本文就根据Spring框架分别对ConcurrentHashMap、Guava Cache、Redis进行阐释如何使用,完整代码已上传到github:https://github.com/searchingbeyond/ssm web

 

1、使用ConcurrentHashMap

1.一、特色说明

  ConcurrentHashMap是JDK自带的,因此不须要多余的jar包;redis

  使用ConcurrentHashMap,是直接使用将数据存放在内存中,而且没有数据过时的概念,也没有数据容量的限制,因此只要不主动清理数据,那么数据将一直不会减小。spring

  另外,ConcurrentHashMap在多线程状况下也是安全的,不要使用HashMap存缓存数据,由于HashMap在多线程操做时容易出现问题。数据库

 

1.二、建立user类

  下面是user类代码:apache

package cn.ganlixin.ssm.model.entity;

import lombok.Data;

@Data
public class UserDO {
    private Integer id;
    private String name;
    private Integer age;
    private Integer gender;
    private String addr;
    private Integer status;
}

  

1.三、建立spring cache的实现类

  建立一个UserCache类(类名随意),实现org.springframework.cache.Cache接口,而后override须要实现的接口方法,主要针对getName、get、put、evict这4个方法进行重写。

  注意,我在缓存user数据时,指定了缓存的规则:key用的是user的id,value就是user对象的json序列化字符。

package cn.ganlixin.ssm.cache.origin;

import cn.ganlixin.ssm.constant.CacheNameConstants;
import cn.ganlixin.ssm.model.entity.UserDO;
import cn.ganlixin.ssm.util.common.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class UserCache implements Cache {

    // 使用ConcurrentHashMap做为数据的存储
    private Map<String, String> storage = new ConcurrentHashMap<>();

    // getName获取cache的名称,存取数据的时候用来区分是针对哪一个cache操做
    @Override
    public String getName() {
        return CacheNameConstants.USER_ORIGIN_CACHE;// 我用一个常量类来保存cache名称
    }

    // put方法,就是执行将数据进行缓存
    @Override
    public void put(Object key, Object value) {
        if (Objects.isNull(value)) {
            return;
        }

        // 注意我在缓存的时候,缓存的值是把对象序列化后的(固然能够修改storage直接存放UserDO类也行)
        storage.put(key.toString(), JsonUtils.encode(value, true));
    }

    // get方法,就是进行查询缓存的操做,注意返回的是一个包装后的值
    @Override
    public ValueWrapper get(Object key) {
        String k = key.toString();
        String value = storage.get(k);
        
        // 注意返回的数据,要和存放时接收到数据保持一致,要将数据反序列化回来。
        return StringUtils.isEmpty(value) ? null : new SimpleValueWrapper(JsonUtils.decode(value, UserDO.class));
    }

    // evict方法,是用来清除某个缓存项
    @Override
    public void evict(Object key) {
        storage.remove(key.toString());
    }

    /*----------------------------下面的方法暂时忽略无论-----------------*/

    @Override
    public Object getNativeCache() { return null; }

    @Override
    public void clear() { }

    @Override
    public <T> T get(Object key, Class<T> type) { return null; }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) { return null; }
}

  

1.四、建立service

  这里就不写贴出UserMapper的代码了,直接看接口就明白了:

package cn.ganlixin.ssm.service;

import cn.ganlixin.ssm.model.entity.UserDO;

public interface UserService {

    UserDO findUserById(Integer id);

    Boolean removeUser(Integer id);

    Boolean addUser(UserDO user);

    Boolean modifyUser(UserDO user);
}

  实现UserService,代码以下:

package cn.ganlixin.ssm.service.impl;

import cn.ganlixin.ssm.constant.CacheNameConstants;
import cn.ganlixin.ssm.mapper.UserMapper;
import cn.ganlixin.ssm.model.entity.UserDO;
import cn.ganlixin.ssm.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Objects;

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    @Cacheable(value = CacheNameConstants.USER_ORIGIN_CACHE, key = "#id")
    public UserDO findUserById(Integer id) {
        try {
            log.info("从DB查询id为{}的用户", id);
            return userMapper.selectById(id);
        } catch (Exception e) {
            log.error("查询用户数据失败,id:{}, e:{}", id, e);
        }

        return null;
    }

    @Override
    @CacheEvict(
            value = CacheNameConstants.USER_ORIGIN_CACHE,
            key = "#id",
            condition = "#result != false"
    )
    public Boolean removeUser(Integer id) {
        if (Objects.isNull(id) || id <= 0) {
            return false;
        }

        try {
            int cnt = userMapper.deleteUserById(id);
            return cnt > 0;
        } catch (Exception e) {
            log.error("删除用户数据失败,id:{}, e:{}", id, e);
        }

        return false;
    }

    @Override
    public Boolean addUser(UserDO user) {
        if (Objects.isNull(user)) {
            log.error("添加用户异常,参数不能为null");
            return false;
        }

        try {
            return userMapper.insertUserSelectiveById(user) > 0;
        } catch (Exception e) {
            log.error("添加用户失败,data:{}, e:{}", user, e);
        }

        return false;
    }

    @Override
    @CacheEvict(
            value = CacheNameConstants.USER_ORIGIN_CACHE,
            key = "#user.id",
            condition = "#result != false"
    )
    public Boolean modifyUser(UserDO user) {
        if (Objects.isNull(user) || Objects.isNull(user.getId()) || user.getId() <= 0) {
            log.error("更新用户异常,参数不合法,data:{}", user);
            return false;
        }

        try {
            return userMapper.updateUserSelectiveById(user) > 0;
        } catch (Exception e) {
            log.error("添加用户失败,data:{}, e:{}", user, e);
        }

        return false;
    }
}

 

1.五、@Cachable、@CachePut、@CacheEvict

  上面方法声明上有@Cachable、@CachePut、@CacheEvict注解,用法以下:

  @Cachable注解的方法,先查询缓存中有没有,若是已经被缓存,则从缓存中查询数据并返回给调用方;若是查缓存没有查到数据,就执行被注解的方法(通常是从DB中查询),而后将从DB查询的结果进行缓存,而后将结果返回给调用方;

  @CachePut注解的方法,不会查询缓存是否存在要查询的数据,而是每次都执行被注解的方法,而后将结果的返回值先缓存,而后返回给调用方;

  @CacheEvict注解的方法,每次都会先执行被注解的方法,而后再将缓存中的缓存项给清除;

  这三个注解都有几个参数,分别是value、key、condition,这些参数的含义以下:

  value,用来指定将数据放入哪一个缓存,好比上面是将数据缓存到UserCache中;

  key,表示放入缓存的key,也就是UserCache中的put方法的key;

  condition,表示数据进行缓存的条件,condition为true时才会缓存数据;

  最后缓存项的值,这个值是指的K-V的V,其实只有@Cachable和@CachePut才须要注意缓存项的值(也就是put方法的value),缓存项的值就是被注解的方法的返回值。

 

1.六、建立一个controller进行测试

  代码以下:

package cn.ganlixin.ssm.controller;

import cn.ganlixin.ssm.enums.ResultStatus;
import cn.ganlixin.ssm.model.Result;
import cn.ganlixin.ssm.model.entity.UserDO;
import cn.ganlixin.ssm.service.UserService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Objects;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    @GetMapping(value = "/getUserById")
    public Result<UserDO> getUserById(Integer id) {
        UserDO data = userService.findUserById(id);

        if (Objects.isNull(data)) {
            return new Result<>(ResultStatus.DATA_EMPTY.getCode(), ResultStatus.DATA_EMPTY.getMsg(), null);
        }

        return new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), data);
    }

    @PostMapping(value = "removeUser")
    public Result<Boolean> removeUser(Integer id) {
        Boolean res = userService.removeUser(id);
        return res ? new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), true)
                : new Result<>(ResultStatus.FAILED.getCode(), ResultStatus.FAILED.getMsg(), false);
    }

    @PostMapping(value = "addUser")
    public Result<Boolean> addUser(@RequestBody UserDO user) {
        Boolean res = userService.addUser(user);

        return res ? new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), true)
                : new Result<>(ResultStatus.FAILED.getCode(), ResultStatus.FAILED.getMsg(), false);
    }

    @PostMapping(value = "modifyUser")
    public Result<Boolean> modifyUser(@RequestBody UserDO user) {
        Boolean res = userService.modifyUser(user);

        return res ? new Result<>(ResultStatus.OK.getCode(), ResultStatus.OK.getMsg(), true)
                : new Result<>(ResultStatus.FAILED.getCode(), ResultStatus.FAILED.getMsg(), false);
    }

}

  

 

 

2、使用Guava Cache实现

  使用Guava Cache实现,其实只是替换ConcurrentHashMap,其余的逻辑都是同样的。

2.一、特色说明

  Guava是google开源的一个集成包,用途特别广,在Cache也占有一席之地,对于Guava Cache的用法,若是没有用过,能够参考:guava cache使用方式

  使用Guava Cache,能够设置缓存的容量以及缓存的过时时间

 

2.二、实现spring cache接口

  仍旧使用以前的示例,从新建立一个Cache实现类,这里对“Book”进行缓存,因此缓存名称为BookCache。

package cn.ganlixin.ssm.cache.guava;

import cn.ganlixin.ssm.constant.CacheNameConstants;
import cn.ganlixin.ssm.model.entity.BookDO;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 书籍数据缓存
 */
@Component
public class BookCache implements org.springframework.cache.Cache {

    // 下面的Cache是Guava对cache
    private Cache<String, BookDO> storage;

    @PostConstruct
    private void init() {
        storage = CacheBuilder.newBuilder()
                // 设置缓存的容量为100
                .maximumSize(100)
                // 设置初始容量为16
                .initialCapacity(16)
                // 设置过时时间为写入缓存后10分钟过时
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build();
    }

    @Override
    public String getName() {
        return CacheNameConstants.BOOK_GUAVA_CACHE;
    }

    @Override
    public ValueWrapper get(Object key) {
        if (Objects.isNull(key)) {
            return null;
        }

        BookDO data = storage.getIfPresent(key.toString());
        return Objects.isNull(data) ? null : new SimpleValueWrapper(data);
    }

    @Override
    public void evict(Object key) {
        if (Objects.isNull(key)) {
            return;
        }

        storage.invalidate(key.toString());
    }

    @Override
    public void put(Object key, Object value) {
        if (Objects.isNull(key) || Objects.isNull(value)) {
            return;
        }

        storage.put(key.toString(), (BookDO) value);
    }

    /*-----------------------忽略下面的方法-----------------*/

    @Override
    public <T> T get(Object key, Class<T> type) { return null; }

    @Override
    public Object getNativeCache() { return null; }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) { return null; }

    @Override
    public void clear() { }
}

  

 

3、使用Redis实现

3.一、特色说明

  因为ConcurrentHashMap和Guava Cache都是将数据直接缓存在服务主机上,很显然,缓存数据量的多少和主机的内存直接相关,通常不会用来缓存特别大的数据量;

  而比较大的数据量,咱们通常用Redis进行缓存。

  使用Redis整合Spring Cache,其实和ConcurrentHashMap和Guava Cache同样,只是在实现Cache接口的类中,使用Redis进行存储接口。

 

3.二、建立Redis集群操做类

  建议本身搭建一个redis测试集群,能够参考:

  redis配置以下(application.properties)

#redis集群的节点信息
redis.cluster.nodes=192.168.1.3:6379,192.168.1.4:6379,192.168.1.5:6379
# redis链接池的配置
redis.cluster.pool.max-active=8
redis.cluster.pool.max-idle=5
redis.cluster.pool.min-idle=3

  

  代码以下:

package cn.ganlixin.ssm.config;

import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

import java.util.Set;
import java.util.stream.Collectors;

@Configuration
public class RedisClusterConfig {

    private static final Logger log = LoggerFactory.getLogger(RedisClusterConfig.class);

    @Value("${redis.cluster.nodes}")
    private Set<String> redisNodes;

    @Value("${redis.cluster.pool.max-active}")
    private int maxTotal;

    @Value("${redis.cluster.pool.max-idle}")
    private int maxIdle;

    @Value("${redis.cluster.pool.min-idle}")
    private int minIdle;

    // 初始化redis配置
    @Bean
    public JedisCluster redisCluster() {

        if (CollectionUtils.isEmpty(redisNodes)) {
            throw new RuntimeException();
        }

        // 设置redis集群的节点信息
        Set<HostAndPort> nodes = redisNodes.stream().map(node -> {
            String[] nodeInfo = node.split(":");
            if (nodeInfo.length == 2) {
                return new HostAndPort(nodeInfo[0], Integer.parseInt(nodeInfo[1]));
            } else {
                return new HostAndPort(nodeInfo[0], 6379);
            }
        }).collect(Collectors.toSet());

        // 配置链接池
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxTotal);
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);

        // 建立jediscluster,传入节点列表和链接池配置
        JedisCluster cluster = new JedisCluster(nodes, jedisPoolConfig);
        log.info("finish jedis cluster initailization");

        return cluster;
    }
}

  

 3.三、建立spring cache实现类

  只须要在涉及到数据操做的时候,使用上面的jedisCluster便可,这里存在redis的数据,我设置为Music,因此叫作music cache:

package cn.ganlixin.ssm.cache.redis;

import cn.ganlixin.ssm.constant.CacheNameConstants;
import cn.ganlixin.ssm.model.entity.MusicDO;
import cn.ganlixin.ssm.util.common.JsonUtils;
import com.google.common.base.Joiner;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisCluster;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.Callable;

@Component
public class MusicCache implements Cache {

    // 使用自定义的redisCluster
    @Resource
    private JedisCluster redisCluster;

    /**
     * 构建redis缓存的key
     *
     * @param type   类型
     * @param params 参数(不定长)
     * @return 构建的key
     */
    private String buildKey(String type, Object... params) {
        // 本身设定构建方式
        return Joiner.on("_").join(type, params);
    }

    @Override
    public String getName() {
        return CacheNameConstants.MUSIC_REDIS_CACHE;
    }

    @Override
    public void put(Object key, Object value) {
        if (Objects.isNull(value)) {
            return;
        }

        // 本身定义数据类型和格式
        redisCluster.set(buildKey("music", key), JsonUtils.encode(value, true));
    }

    @Override
    public ValueWrapper get(Object key) {
        if (Objects.isNull(key)) {
            return null;
        }

        // 本身定义数据类型和格式
        String music = redisCluster.get(buildKey("music", key));
        return StringUtils.isEmpty(music) ? null : new SimpleValueWrapper(JsonUtils.decode(music, MusicDO.class));
    }

    @Override
    public void evict(Object key) {
        if (Objects.isNull(key)) {
            return;
        }

        redisCluster.del(buildKey("music", key));
    }

    @Override
    public <T> T get(Object key, Class<T> type) { return null; }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) { return null; }

    @Override
    public void clear() { }

    @Override
    public Object getNativeCache() { return null; }
}

  

总结

  使用spring cache的便捷之处在于@Cachable、@CachePut、@CacheEvict等几个注解的使用,可让数据的处理变得更加的便捷,但其实,也并非很便捷,由于咱们须要对数据的存储格式进行设定,另外还要根据不一样状况来选择使用哪种缓存(ConcurrentHashMap、Guava Cache、Redis?);

  其实使用@Cachable、@CachePut、@CacheEvict也有不少局限的地方,好比删除某项数据的时候,我但愿清空多个缓存,由于这一项数据关联的数据比较多,此时要么在实现spring cache的接口方法上进行这些操做,可是这就涉及到在一个cache service中操做另一个cache。

  针对上面说的状况,就不推荐使用spring cache,而是应该本身手动实现缓存的处理,这样能够作到条理清晰;可是通常的状况,spring cache已经可以胜任了。

相关文章
相关标签/搜索