目录
前言:每次都从数据库存取数据性能较低,可以把一些常用信息放在缓存,提高取数据的效率。Redis是常用的缓存机制。
1. Redis简介
Redis全称为Remote Dictionary Server(远程数据服务),是一款开源的基于内存的键值对存储系统。Reids支持各种数据类型,包括字符串、哈希、列表、集合、有序集合,而不像Memcached要求的键和值都是字符串。同时由于Redis是基于内存的方式,免去了磁盘I/O速度的影响,因此其读写性能极高。
使用前需要先下载安装redis,每次使用时需要先提前开启resid系统,也可以通过设置让redis自动启动。
1.1 数据类型
redis支持多种数据类型:
- String
- Hash
- List
- Set
- ZSet
1.2 redis的使用
redis使用可以直接在调用类注入redistemplate,操作哪种数据类型就调用对应的operations。
\\操作String类型
redisTemplate。opsForValue().set("key", "value");
\\操作Hash类型
redisTemplate。opsForHash().put("hash", "test", "hello");
redis对于复杂的实体类进行操作时,需要进行序列化处理。
2. Springboot整合Reids
Springboot整合Reids常见方法是通过redisTemplate实现的,但除此外,springboot中自带redis repository接口,两种实现方法的依赖和配置步骤一样(即1,2,3步骤一样),java代码部分不同,先讲解redisTemplate方法,再添加repository方法。
2.1 添加依赖
<!-- redis配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lecttuce 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.2 修改配置
现在spring中配置redis。需要设置ip,port和lettuce连接池信息。因为我们是在本地,所以host地址是127.0.0.1。
spring:
application:
name: buildBaseFrame
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: online_exam
password: xxx
url: xxxxx
hikari:
# 池中维护的最小空闲连接数,默认为 10 个
minimum-idle: 10
# 池中最大连接数,包括闲置和使用中的连接,默认为 10 个
maximum-pool-size: 10
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 32
# 连接池中的最大空闲连接
max-idle: 16
# 连接池中的最小空闲连接
min-idle: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
# max-wait: 5000ms
timeout: 5000
password: xxxx
jackson:
date-format: yyyy-MM-dd HH:mm:ss
serialization:
indent_output: false
fail_on_empty_beans: false
defaultPropertyInclusion: NON_NULL
deserialization:
fail_on_unknown_properties: false
parser:
allow_unquoted_control_chars: true
allow_single_quotes: true
mybatis-plus是springboot中常用的数据库插件,我们还需要在mybatis中配置redis。
# mapper对应的xml文件存放路径,开启 mybatis 下划线命名法自动到驼峰命名法映射
mybatis-plus:
mapper-locations: classpath*:com/example/buildBaseFrame/infrastructure/*/persistence/mysql/mapper/xml/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: true
# Redis缓存配置
cache:
enabled: true # 开启Redis缓存
type: redis # Redis缓存类型
ttl: 60000 # Redis缓存过期时间,单位为毫秒,默认值为-1,即永不过期
flush-interval: 60000 # Redis缓存刷新时间,单位为毫秒,默认值为-1,即永不刷新
size: 1000 # Redis缓存大小,默认值为0,即不限制缓存大小
global-config:
db-config:
# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以不在逻辑删除字段配置 @TableLogic)(若某表不使用逻辑删除,不在表中添加该字段即可)
logic-delete-field: dataStatus
# 逻辑已删除值(默认为 1)
logic-delete-value: id
# 逻辑未删除值(默认为 0)
logic-not-delete-value: 0
enabled字段为true时才会开启缓存,springboot支持多种缓存策略,这里我们使用redis。
2.3 开启缓存
@SpringBootApplication
@EnableCaching // 开启缓存
public class BuildBaseFrameApplication {
private static ApplicationContext context;
public static ApplicationContext getContext() {
return context;
}
public static void main(String[] args) {
SpringApplication.run(BuildBaseFrameApplication.class, args);
}
}
使用@EnableCaching开启缓存,也可以写在RedisConfig上。
2.4 Config设置
为了在springboot中使用redis,可以通过@Configuration注解进行配置。
在redisconfig中主要需要定义一个redisTemplate模板和一个缓存管理器cacheManager。主要是为了让object和redis序列能够互相转换。
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
* 自定义RedisTemplate
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//大多数情况,都是选用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用JSON的序列化对象,对数据key和value进行序列化转换
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
//ObjectMapper是Jackson的一个工作类,顾名思义他的作用是将JSON映射到Java对象即反序列化,或将Java对象映射到JSON即序列化
ObjectMapper mapper = new ObjectMapper();
// 设置序列化时的可见性,第一个参数是选择序列化哪些属性,比如时序列化setter?还是filed?h第二个参数是选择哪些修饰符权限的属性来序列化,比如private或者public,这里的any是指对所有权限修饰的属性都可见(可序列化)
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 设置出现故障即错误的类型,第一个是指验证程序,此时的参数为无需验证,其他参数可以查看源码了解(作者还在啃源码中),第二是指该类不能为final修饰,否则将会报错
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
// 设置RedisTemplate模板的序列化方式为jacksonSeial
template.setDefaultSerializer(jackson2JsonRedisSerializer);
return template;
}
/**
* 自定义缓存管理器
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 创建String和JSON序列化对象,分别对key和value的数据进行类型转换
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(mapper);
// 自定义缓存数据序列化方式和有效期限
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1)) // 设置缓存有效期为1天
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer)) // 使用strSerializer对key进行数据类型转换
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial)) // 使用jacksonSeial对value的数据类型进行转换
.disableCachingNullValues(); // 对空数据不操作
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
}
为了在mybatisplus中使用redis,还需要配置文件连接。里面包含了基本的数据操作:
putObject、getObject、removeObject、clear、getSize。
@Slf4j
public class MybatisRedisCache implements Cache {
// 读写锁
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private RedisTemplate redisTemplate;
private RedisTemplate getRedisTemplate(){
//通过ApplicationContextHolder工具类获取RedisTemplate
if (redisTemplate == null) {
redisTemplate = (RedisTemplate) ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
private final String id;
public MybatisRedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
//使用redis的Hash类型进行存储
getRedisTemplate().opsForHash().put(id,key.toString(),value);
}
@Override
public Object getObject(Object key) {
try {
//根据key从redis中获取数据
return getRedisTemplate().opsForHash().get(id,key.toString());
} catch (Exception e) {
e.printStackTrace();
log.error("缓存出错 ");
}
return null;
}
@Override
public Object removeObject(Object key) {
if (key != null) {
getRedisTemplate().delete(key.toString());
}
return null;
}
@Override
public void clear() {
log.debug("清空缓存");
if (redisTemplate == null) {
redisTemplate = (RedisTemplate<String, Object>) ApplicationContextHolder.getBean("redisTemplate");
}
try {
Set<String> keys = scanMatch(this.id);
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
} catch (Exception e) {
log.error("清空缓存", e);
}
}
private static final Integer SCAN_COUNT = 10000;
/**
* 使用scan遍历key
* 为什么不使用keys 因为Keys会引发Redis锁,并且增加Redis的CPU占用,特别是数据庞大的情况下。这个命令千万别在生产环境乱用。
* 支持redis单节点和集群调用
*
* @param matchKey
* @return
*/
public Set<String> scanMatch(String matchKey) {
Set<String> keys = new HashSet();
RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
RedisConnection redisConnection = connectionFactory.getConnection();
Cursor<byte[]> scan = null;
//集群
if(redisConnection instanceof JedisClusterConnection){
RedisClusterConnection clusterConnection = connectionFactory.getClusterConnection();
Iterable<RedisClusterNode> redisClusterNodes = clusterConnection.clusterGetNodes();
Iterator<RedisClusterNode> iterator = redisClusterNodes.iterator();
while (iterator.hasNext()) {
RedisClusterNode next = iterator.next();
scan = clusterConnection.scan(next, ScanOptions.scanOptions().match(matchKey).count(Integer.MAX_VALUE).build());
while (scan.hasNext()) {
keys.add(new String(scan.next()));
}
try {
if(scan !=null){
scan.close();
}
} catch (Exception e) {
log.error("scan遍历key关闭游标异常",e);
}
}
return keys;
}
//单机
if(redisConnection instanceof JedisConnection){
scan = redisConnection.scan(ScanOptions.scanOptions().match(matchKey + "*").count(SCAN_COUNT).build());
while (scan.hasNext()) {
//找到一次就添加一次
keys.add(new String(scan.next()));
}
try {
if (scan != null) {
scan.close();
}
} catch (Exception e) {
log.error("scan遍历key关闭游标异常", e);
}
return keys;
}
return keys;
}
@Override
public int getSize() {
Long size = (Long) getRedisTemplate().execute((RedisCallback<Long>) RedisServerCommands::dbSize);
return size.intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
}
在clear中需要对当前id的用户进行缓存清空,因此需要在redis数据库中遍历查找这个id。redis遍历有scan和keys两种策略,但通常不使用keys,因为Keys会引发Redis锁,并且增加Redis的CPU占用,特别是数据庞大的情况下。scanMatch中使用了两种遍历策略,单机或者集群。
2.5 使用redis缓存
这里我们在mapper阶段使用redis缓存。
@Mapper
@CacheNamespace(implementation= MybatisRedisCache.class,eviction=MybatisRedisCache.class) // 开启二级缓存
public interface UserPoMapper extends BaseMapper<UserPo> {
/**
* 测试xml方式绑定查询
*/
@Deprecated
UserPo findThis(@Param("id") Long id);
UserPo findByName(@Param("name") String name);
}
使用@CacheNamespace注解,定义了这个mapper使用MybatisRedisCache.class作为缓存类。
当用户进行查询操作时,第一次查询从数据库查询,查询数据被存入redis数据库,后续查询时会从redis数据库取。
2.6 redis repository方法
Redis Repositories 可以让我们在Redis哈希类型中转换和存储Java实体,支持自定义映射策略和二级索引。
注意:
- 使用该功能,需要保证Redis服务器版本在2.8.0之上
- 该功能并不支持Spring事务(RedisTemplate同样也不参与Spring事务,不过RedisTemplate可以用过在配置类添加@EnableTransactionManagement以启用事务支持)
- Redis Repositories的存储结构为<String, Hash>
- Redis Repositories的键构成形式:keyspace:id
2.6.1 修改config文件
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
}
2.6.2 修改实体类
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@Data
@RedisHash("User")
public class User {
@Id
private String id;
private String name;
}
注意:
- 使用@RedisHash(“User”)启用缓存
- @Id标注了用于组成redis键的属性,当然如果实体中有名为id的属性,那么也可以不用设置@Id
- redis中保存的键的形式为:keyspace:id,RedisHash中设置的值即是keyspace,若id="5"的话,那么redis的键可能为User:5。因此不同的实体尽量不要使用重复的keyspace。
2.6.3 repository使用redis
@Repository
public interface UserRedis extends CrudRepository<User, String> {
}
直接继承CrudRepository<User, String>。
2.7 keyGenerator
自定义reids存储的key生成规则
2.7.1 自定义规则
public class MyKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
Object param = params[0];
// 参数为map自定义key=类名+方法名+map的key-value值
if (param instanceof Map) {
StringBuilder builder = new StringBuilder();
// 分隔符
String sp = ".";
builder.append(target.getClass().getSimpleName()).append(sp);
builder.append(method.getName()).append(sp);
Map<String, Object> map = (Map<String, Object>) param;
if (map.isEmpty()) {
return builder.toString();
}
for (String key : map.keySet()) {
builder.append(key).append("-").append(map.get(key)).append(sp);
}
return builder.toString();
}
return new SimpleKey(params);
}
}
2.7.1 使用自定义规则
在配置类或启动类中,使用@EnableCaching注解和@EnableRedisRepositories注解,并指定keyGenerator为自定义的CustomKeyGenerator
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
@SpringBootApplication
@EnableCaching
@EnableRedisRepositories(keyGenerator = "customKeyGenerator")
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}