我们日常生活中,经常会接触听到缓存这个词,例如,浏览器清空缓存,处理器缓存大小,磁盘缓存等等。经过分类,可以将缓存分为:
用缓存,主要有解决 高性能 与 高并发 与 减少数据库压力。缓存本质就是将数据存储在内存中
,当数据没有发生本质变化的时候,我们应尽量避免直接连接数据库进行查询,因为并发高时很可能会将数据库压塌,而是应去缓存中读取数据,只有缓存中未查找到时再去数据库中查询,这样就大大降低了数据库的读写次数,增加系统的性能和能提供的并发量
Redis 是一个高性能的 Key-Value 数据库,它是完全开源免费的,而且 Redis 是一个 NoSQL 类型数据库,是为了解决 高并发、高扩展,大数据存储 等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库。但是,它也是不能替代关系型数据库,只能作为特定环境下的扩充。
Redis 支持的数据结构类型包括:
缓存穿透: 指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
缓存穿透几种解决办法:
缓存击穿: 某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
缓存击穿几种解决办法:
缓存雪崩: 当缓存服务器重启、或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力,造成数据库后端故障,从而引起应用服务器雪崩。
缓存雪崩几种解决办法:
使用缓存很大可能导致数据不一致问题,如下:
所以使用缓存时候,应该结合实际情况,考虑缓存的数据是否有一致性需求。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
其余的
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.1version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.3.1.tmpversion>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.54version>
dependency>
#redis单点配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/goods_system?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=true&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost #redis地址
port: 6379 #redis端口
database: 0 #redis索引(0-15,默认为0)
timeout: 1000 #redis连接超时时间
lettuce: #使用lettuce连接池
pool:
max-active: 20 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 #连接池中的最小空闲连接
max-idle: 10 #连接池中的最大空闲连接
spring:
sentinel: #哨兵配置
master: "my-master"
nodes: "192.168.10.261:6379,192.168.10.262:6379,192.168.10.263:6379"
database: 0 #redis索引(0-15,默认为0)
timeout: 1000 #redis连接超时时间
lettuce: #使用lettuce连接池
pool:
max-active: 20 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 #连接池中的最小空闲连接
max-idle: 10 #连接池中的最大空闲连接
#redis集群配置
spring:
redis:
cluster:
max-redirects: 5
nodes: "192.168.10.261:6379,192.168.10.262:6379,192.168.10.263:6379"
database: 0 #redis索引(0-15,默认为0)
timeout: 1000 #redis连接超时时间
lettuce: #使用lettuce连接池
pool:
max-active: 20 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 #连接池中的最小空闲连接
max-idle: 10 #连接池中的最大空闲连接
package com.example.springbootrediscache.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
/**
* @Author Emperor Kang
* @ClassName RedisConfig
* @Description redis配置类
* @Date 2022/9/19 14:20
* @Version 1.0
* @Motto 让营地比你来时更干净
*/
@Configuration
public class RedisConfig {
/**
* 配置缓存管理器
* @param factory Redis 线程安全连接工厂
* @return 缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory){
// 生成两套默认配置,通过 Config 对象即可对缓存进行自定义配置
RedisCacheConfiguration userCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 设置过期时间 10 分钟
.entryTtl(Duration.ofMinutes(10))
// 设置缓存前缀
.prefixKeysWith("cache:user:")
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(keyPair())
// 设置 value 序列化
.serializeValuesWith(valuePair());
RedisCacheConfiguration userInfoCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 设置过期时间 30 秒
.entryTtl(Duration.ofSeconds(30))
// 设置缓存前缀
.prefixKeysWith("cache:user_info:")
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(keyPair())
// 设置 value 序列化
.serializeValuesWith(valuePair());
// 返回 Redis 缓存管理器
return RedisCacheManager.builder(factory)
.withCacheConfiguration("user", userCacheConfig)
.withCacheConfiguration("userInfo", userInfoCacheConfig)
.build();
}
/**
* 自定义Redis Key生产策略
* 在使用是, 指定@Cacheable(cacheNames = "user", keyGenerator = "userKeyGenerator")
* @return
*/
@Bean(name = "userKeyGenerator")
public KeyGenerator userKeyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder builder = new StringBuilder();
//类名
builder.append(target.getClass().getName());
//方法名
builder.append(method.getName());
//参数,这里可以挑选用哪参数,不用哪些参数
for (Object param : params) {
builder.append(param);
}
return builder.toString();
}
};
}
/**
* 配置键序列化
* @return StringRedisSerializer
*/
private RedisSerializationContext.SerializationPair<String> keyPair() {
return RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());
}
/**
* 配置值序列化,使用 GenericJackson2JsonRedisSerializer 替换默认序列化
* @return GenericJackson2JsonRedisSerializer
*/
private RedisSerializationContext.SerializationPair<Object> valuePair() {
return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
}
}
package com.example.springbootrediscache.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.springbootrediscache.entity.User;
import com.example.springbootrediscache.mapper.UserMapper;
import com.example.springbootrediscache.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
*
* 用户表 服务实现类
*
*
* @author zkk
* @since 2022-09-19
*/
@Service
@CacheConfig(cacheNames = "user")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
/**
* 新增用户
* @param user 账户
*/
@Override
public void addUser(User user) {
userMapper.insert(user);
}
/**
* 查询用户
* @param username 用户名
* @return
*/
@Override
@Cacheable(key = "#username")
//@Cacheable(keyGenerator = "userKeyGenerator")
public User getUserByUsername(String username) {
QueryWrapper<User> objectQueryWrapper = new QueryWrapper<>();
objectQueryWrapper.eq("name",username);
return userMapper.selectOne(objectQueryWrapper);
}
/**
* 更新用户
* @param user 用户信息
* @return
*/
@Override
@CachePut(key = "#user.name")
public User updateUser(User user) {
QueryWrapper<User> updateWrapper = new QueryWrapper<>();
updateWrapper.eq("name",user.getName());
//更新
userMapper.update(user,updateWrapper);
//将更新后的值返回
QueryWrapper<User> objectQueryWrapper = new QueryWrapper<>();
objectQueryWrapper.eq("name",user.getName());
return userMapper.selectOne(objectQueryWrapper);
}
/**
* 删除
* @param username 用户名
*/
@Override
@CacheEvict(key = "#username")
public void deleteByUsername(String username) {
QueryWrapper<User> objectQueryWrapper = new QueryWrapper<>();
objectQueryWrapper.eq("name",username);
userMapper.delete(objectQueryWrapper);
}
}
注解说明
常用配置参数
实体类
package com.example.springbootrediscache.entity;
import java.time.LocalDateTime;
import java.io.Serializable;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
*
* 用户表
*
*
* @author zkk
* @since 2022-09-19
*/
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 账号
*/
private String account;
/**
* 密码
*/
private String password;
/**
* 姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 创建人
*/
private Long createUser;
/**
* 状态(0--正常1--冻结)
*/
private Boolean status;
/**
* 删除状态(0,正常,1已删除)
*/
private Boolean delFlag;
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public Long getCreateUser() {
return createUser;
}
public void setCreateUser(Long createUser) {
this.createUser = createUser;
}
public Boolean getStatus() {
return status;
}
public void setStatus(Boolean status) {
this.status = status;
}
public Boolean getDelFlag() {
return delFlag;
}
public void setDelFlag(Boolean delFlag) {
this.delFlag = delFlag;
}
}
controller
package com.example.springbootrediscache.controller;
import com.example.springbootrediscache.entity.User;
import com.example.springbootrediscache.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
*
* 用户表 前端控制器
*
*
* @author zkk
* @since 2022-09-19
*/
@RestController
@RequestMapping("/sys/user")
public class UserController {
@Autowired
private IUserService userService;
/**
* 新增
* @param user
* @return
*/
@PostMapping("/add")
public Object addUser(@RequestBody User user){
userService.addUser(user);
return true;
}
/**
* 查询
* @param username
* @return
*/
@GetMapping("/get")
public Object getUserByUsername(@RequestParam String username){
return userService.getUserByUsername(username);
}
/**
* 修改
* @param user
* @return
*/
@PostMapping("/update")
public Object updateUser(@RequestBody User user){
return userService.updateUser(user);
}
/**
* 删除
* @param username
* @return
*/
@PostMapping("/delete")
public Object deleteByUsername(@RequestParam("username") String username){
userService.deleteByUsername(username);
return true;
}
}
代码生成器
package com.example.springbootrediscache.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
*
* 读取控制台内容
*
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
// gc.setOutputDir(projectPath + "/src/main/java");
gc.setOutputDir("D:\\work_space\\2022\\spring_cloud_all\\SpringBootRedisCache\\src\\main\\java");
gc.setAuthor("zkk");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/goods_system?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=true&characterEncoding=UTF-8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner("模块名"));
pc.setParent("com.example.springbootrediscache");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return "D:\\work_space\\2022\\spring_cloud_all\\SpringBootRedisCache\\src\\main\\resources\\mapper\\" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录,自定义目录用");
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
//strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
package com.example.springbootrediscache;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@MapperScan("com.example.springbootrediscache.mapper")
@EnableCaching
public class SpringBootRedisCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRedisCacheApplication.class, args);
}
}
SpringBoot 使用 @Cacheable 可以方便的管理缓存数据,在不指定 key 属性的情况下,默认使用 SimpleKeyGenerator 生成 key。除此之外,我们也可以自定义实现 KeyGenerator 接口,生成自己的 key 名称策略。
MySimpleKey类的作用是存放参数数据,必须实现equals、hashCode。如果需要自定义key格式,同样需要实现toString接口,下面的例子是把参数用逗号分隔。
public class MySimpleKey implements Serializable {
public static final MySimpleKey EMPTY = new MySimpleKey(new Object[0]);
private final Object[] params;
private transient int hashCode;
public MySimpleKey(Object... elements) {
Assert.notNull(elements, "Elements must not be null");
this.params = (Object[])elements.clone();
this.hashCode = Arrays.deepHashCode(this.params);
}
public boolean equals(@Nullable Object other) {
return this == other || other instanceof MySimpleKey && Arrays.deepEquals(this.params, ((MySimpleKey)other).params);
}
public final int hashCode() {
return this.hashCode;
}
public String toString() {
return StringUtils.arrayToCommaDelimitedString(this.params);
}
}
MyKeyGenerator 实现 KeyGenerator 的接口,里面只有一个 generate 方法
public class MyKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object o, Method method, Object... objects) {
if (objects.length == 0) {
return MySimpleKey.EMPTY;
} else {
if (objects.length == 1) {
Object param = objects[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new MySimpleKey(objects);
}
}
}
定义MyKeyGenerator Bean:
@Component
public class MyRedisConf {
@Bean
public MyKeyGenerator myKeyGenerator(){
return new MyKeyGenerator();
}
}
在 @Cacheable 配置 keyGenerator 属性,值就是前面配置的Bean名称
@Override
@Cacheable(value = {"REDIS:GETSTRING3"}, keyGenerator = "myKeyGenerator")
public String getString3(String tag, String name) {
return tag + " " + name;
}
为了便于key的不重复,我们可以手动设置key有类名、方法名、参数等组合
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method .name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表 | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字,可以直接#参数名,也可以使用#p0或#a0的形式,0代表参数的索引 | #iban、#a0、#p0 |
result | evaluation context | 方法执行后的返回值 | #result |
key = "#root.targetClass.simpleName+':'+#root.methodName+':'+#param"
自定义CacheKeyGenerator 实现KeyGenerator
public class CacheKeyGenerator implements KeyGenerator {
/**
* (非 Javadoc)
*
* Title: generate
*
*
* @param target
* @param method
* @param params
* @return
* @see org.springframework.cache.interceptor.KeyGenerator#generate(java.lang.Object,
* java.lang.reflect.Method, java.lang.Object[])
*/
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName()).append(":").append(method.getName()).append(":");
if (params.length == 0) {
return key.toString();
}
for (int i = 0; i < params.length; i++) {
Object param = params[i];
if (param == null || param instanceof LogableParam) {
del(key);
} else if (ClassUtils.isPrimitiveArray(param.getClass())) {
int length = Array.getLength(param);
for (int j = 0; j < length; j++) {
key.append(Array.get(param, j));
key.append(',');
}
} else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
key.append(param);
} else {
key.append(param.toString());
}
key.append('-');
}
del(key);
return key.toString();
}
private StringBuilder del(StringBuilder stringBuilder) {
if (stringBuilder.toString().endsWith("-")) {
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
return stringBuilder;
}
}
在之前的代码中有相应的自定义KeyGenerator使用,下面仅供参