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

  对于频繁访问,需要缓存的数据,我们一般是这样做的:

  1、当收到查询请求,先去查询缓存,如果缓存中查询到数据,那么直接将查到的数据作为响应数据;

  2、如果缓存中没有找到要查询的数据,那么就从其他地方,比如数据库中查询出来,如果从数据库中查到了数据,就将数据放入缓存后,再将数据返回,下一次可以直接从缓存查询;

  这里就不进一步探究“缓存穿透”的问题,有兴趣可以自己学习一下。

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

一、使用ConcurrentHashMap

1.1、特点说明

  ConcurrentHashMap是JDK自带的,所以不需要多余的jar包;

  使用ConcurrentHashMap,是直接使用将数据存放在内存中,并且没有数据过期的概念,也没有数据容量的限制,所以只要不主动清理数据,那么数据将一直不会减少。

  另外,ConcurrentHashMap在多线程情况下也是安全的,不要使用HashMap存缓存数据,因为HashMap在多线程操作时容易出现问题。

1.2、创建user类

  下面是user类代码:

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.3、创建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.4、创建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.5、@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.6、创建一个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);
} }

  

二、使用Guava Cache实现

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

2.1、特点说明

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

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

2.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() { }
}

  

 

三、使用Redis实现

3.1、特点说明

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

  而比较大的数据量,我们一般用Redis进行缓存。

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

3.2、创建Redis集群操作类

  建议自己搭建一个redis测试集群,可以参考:https://www.cnblogs.com/-beyond/p/10991139.html

  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.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已经能够胜任了。

Spring配置cache(concurrentHashMap,guava cache、redis实现)附源码的更多相关文章

  1. SSM 三大框架系列:Spring 5 + Spring MVC 5 + MyBatis 3.5 整合(附源码)

    之前整理了一下新版本的 SSM 三大框架,这篇文章是关于它的整合过程和项目源码,版本号分别为:Spring 5.2.2.RELEASE.SpringMVC 5.2.2.RELEASE.MyBatis ...

  2. Spring Cloud Zuul 限流详解(附源码)(转)

    在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Cloud中如何实现限流. 在 Zuul 上实现限流是个不错的选择,只需要编写一个过滤器就可以了,关键在于如何实现限流的算法. ...

  3. Spring Cloud(十二):Spring Cloud Zuul 限流详解(附源码)(转)

    前面已经介绍了很多zuul的功能,本篇继续介绍它的另一大功能.在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Cloud中如何实现限流. 在 Zuul 上实现限流是个不错的选 ...

  4. 浅尝Spring注解开发_AOP原理及完整过程分析(源码)

    浅尝Spring注解开发_AOP原理及完整过程分析(源码) 浅尝Spring注解开发,基于Spring 4.3.12 分析AOP执行过程及源码,包含AOP注解使用.AOP原理.分析Annotation ...

  5. 豌豆夹Redis解决方案Codis源码剖析:Proxy代理

    豌豆夹Redis解决方案Codis源码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描述: Codis is a proxy base ...

  6. Redis .Net客户端源码

    1.简单介绍 当前NoSql使用已经极为普遍,无论是Java生态圈,还是.net生态圈.大大小小的Web站点在追求高性能高可靠性方面,不由自主都选择了NoSQL技术作为优先考虑的方面.主流的技术有:H ...

  7. 基于Redis缓存的Session共享(附源码)

    基于Redis缓存的Session共享(附源码) 在上一篇文章中我们研究了Redis的安装及一些基本的缓存操作,今天我们就利用Redis缓存实现一个Session共享,基于.NET平台的Seesion ...

  8. spring学习笔记2---MVC处理器映射(handlerMapping)三种方式(附源码)

    一.根据Beanname访问controller: 在springmmvc-servlet.xml的配置handlermapping中加入beanname,通过该beanname找到对应的contro ...

  9. 豌豆夹Redis解决方案Codis源码剖析:Dashboard

    豌豆夹Redis解决方案Codis源码剖析:Dashboard 1.不只是Dashboard 虽然名字叫Dashboard,但它在Codis中的作用却不可小觑.它不仅仅是Dashboard管理页面,更 ...

随机推荐

  1. PAT甲级——1041 Be Unique

    1041 Be Unique Being unique is so important to people on Mars that even their lottery is designed in ...

  2. mysql模糊匹配like及批量替换replace

    1.mysql 模糊匹配 like 与 not like 用法 : SELECT * FROM `user` where `nickname` LIKE '%测试%' SELECT * FROM `u ...

  3. appium自动化的工作原理(1)

    用appium开发移动端自动化测试脚本这么长时间,还没有认证的了解下它的原理是什么,到底是如何实现的呢? 1.先看一个Appium加载的过程图解(来自:了解appium自动化的工作原理--https: ...

  4. Fastjson主要接口和类库说明

    2.主要的使用入口 Fastjson API入口类是com.alibaba.fastjson.JSON,常用的序列化操作都可以在JSON类上的静态方法直接完成. public static final ...

  5. 在服务器搭建git服务器

    服务端操作 安装Git及创建用户组.用户 123 yum install gitgroupadd gitadduser git -g git 禁止用户git登录 修改/etc/passwd文件: 找到 ...

  6. python语法基础-函数-基础-长期维护

    ###############    函数的定义调用,返回值和返回值接收    ############## def mylen(): s = "myname" i = 0 for ...

  7. 遍历一个文件夹,打印出该文件夹下所有的py文件

    import os def iterbrowse(path): for home, dirs, files in os.walk(path): for filename in files: yield ...

  8. mysql挖掘与探索--表操作命令 1

    1.登录数据库>mysql -u root -p 数据库名称 2.查询所有数据表>show tables; 3.查询表的字段信息>desc 表名称; 4.1添加表字段 alter t ...

  9. [LC] 303. Range Sum Query - Immutable

    Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive ...

  10. nevertheless|magnificent |prosperous|

    ADV 然而;不过You use nevertheless when saying something that contrasts with what has just been said. Mos ...