前言:每次都从数据库存取数据性能较低,可以把一些常用信息放在缓存,提高取数据的效率。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);
    }
}
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐