文章目录
摘要
本文记录了SpringBoot2集成Mybatis采用Redis做缓存测试Mysql百万级数据库查询性能(Mysql和Redis使用Docker模式部署在虚拟机中)的实验过程。测试结果为直接从Mysql查询在350毫秒左右,增加Redis缓存查询在5毫秒左右。Redis作为缓存能够大幅度提高查询效率。整个集成的关键信息包括在POM中增加spring-boot-starter-data-redis依赖,实现Mybatis缓存接口Cache,使用Spring默认的RedisTemplate操作缓存,通过StringRedisSerializer将数据序列化为JSON格式。 下文详细记录了每一步的集成过程。
通过SpringBoot集成Mybatis
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-mybatis-cache2-redis</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
SpringBoot配置
spring:
redis:
host: 192.168.172.144
password:
port: 6379
jedis:
pool:
min-idle: 0
max-active: 8
max-idle: 8
max-wait: 5000
datasource:
url: jdbc:mysql://192.168.172.144:3306/test?characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
mybatis:
configuration:
cache-enabled: true
mapper-locations: classpath:mapper/*.xml
配置Mybatis
编写一个POJO。UserInfo。
编写一个Mapper接口。
@Mapper
@Component
public interface UserInfoMapper {
UserInfo selectById(Long userId);
}
针对Mapper接口进行sql配置。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserInfoMapper">
<!--使用自定义的缓存-->
<cache type="com.example.demo.cache.RedisCache"/>
<resultMap id="userResultMap" type="com.example.demo.entities.UserInfo">
<id property="userId" column="userId" jdbcType="BIGINT" javaType="java.lang.Long"/>
<result property="userName" column="userName" jdbcType="VARCHAR" javaType="java.lang.String"/>
<result property="userAge" column="userAge" jdbcType="INTEGER" javaType="java.lang.Integer"/>
<result property="label" column="label" jdbcType="VARCHAR" javaType="java.lang.String"/>
</resultMap>
<select id="selectById" resultMap="userResultMap" parameterType="java.lang.Long" useCache="true">
SELECT userId,userName,userAge,label
FROM userInfo
WHERE userId = #{id}
</select>
</mapper>
开启Mybatis二级缓存使用Redis做缓存
开启Mybatis二级缓存,自定义Cache
<cache type="com.example.demo.cache.RedisCache"/>
mybatis:
configuration:
cache-enabled: true
编写缓存实现方法:
public class RedisCache implements Cache
public class RedisCache implements Cache {
private String id;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public RedisCache(String id) {
this.id = id;
}
private RedisTemplate<Object, Object> getRedisTemplate(){
return ApplicationContextHolder.getBean("redisTemplate");
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
getRedisTemplate().boundHashOps(getId()).put(key, value);
}
@Override
public Object getObject(Object key) {
return getRedisTemplate().boundHashOps(getId()).get(key);
}
@Override
public Object removeObject(Object key) {
return getRedisTemplate().boundHashOps(getId()).delete(key);
}
@Override
public void clear() {
getRedisTemplate().delete(getId());
}
@Override
public int getSize() {
Long size = getRedisTemplate().boundHashOps(getId()).size();
return size == null ? 0 : size.intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}
使用RedisTemplate进行操作。
@Configuration
public class RedisConfiguration {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String,Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
return redisTemplate;
}
/**
* 设置数据存入 redis 的序列化方式
* @param template
* @param factory
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> template, RedisConnectionFactory factory) {
// 定义 key 的序列化方式为 string
// 需要注意这里Key使用了 StringRedisSerializer,那么Key只能是String类型的,不能为Long,Integer,否则会报错抛异常。
StringRedisSerializer redisSerializer = new StringRedisSerializer();
template.setKeySerializer(redisSerializer);
// 定义 value 的序列化方式为 json
@SuppressWarnings({"rawtypes", "unchecked"})
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
//hash结构的key和value序列化方式
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setEnableTransactionSupport(true);
template.setConnectionFactory(factory);
}
}
采用JSON格式存储。
通过POSTMAN进行测试-正例
请求设置为:
http://localhost:8080/getbyid?id={{$randomInt}}
参数补充说明:
{{$guid}}:添加一个V4风格GUID(如: aa002-44ac-45ca-aae3-52bf19650e2d)
{{$timestamp}}:将当前的时间戳,精确到秒
{{$randomInt}}:添加0和1000之间的随机整数
测试结果:
性能测试结果:
从Mysql查询数据为350毫秒,从redis缓存获取5毫秒。
RedisDesktopManager查看缓存信息
Redis里面缓存的信息:
Redis中具体缓存值查看:
键名:
["org.apache.ibatis.cache.CacheKey",{"multiplier":37,"hashcode":260798286,"checksum":-94844027,"count":6,"updateList":["java.util.ArrayList",["com.example.demo.mapper.UserInfoMapper.selectById",0,2147483647,"SELECT userId,userName,userAge,label\n FROM userInfo\n WHERE userId = ?",["java.lang.Long",173],"SqlSessionFactoryBean"]],"updateCount":6}]
键值:
["java.util.ArrayList",[["com.example.demo.entities.UserInfo",{"userId":173,"userName":"姓名173","userAge":20,"label":"标签173"}]]]
反例测试-试试缓存穿透
修改请求为http://localhost:8080/getbyid?id=-{{$randomInt}},前面增加-。
通过POSTMAN做1000次循环,传入的参数在数据库中都不存在。
大部分数据都是在350毫秒左右,如果已经存在了从redis缓存读取大概在5毫秒左右。
缓存穿透是在如果数据库查询对象为空,则不放进缓存的场景下。本次试验里,缓存为空也记录到redis里了。算是解决了缓存穿透的问题。如果这样的数据太多,会导致redis空间浪费,可以单独设置这样的失效时间更短点,比如60秒。
缓存穿透、缓存雪崩、缓存击穿,集中点在缓存Key是否存在,缓存有效期有很大的关系,比如缓存击穿类似爆款,可以设置永不过期,缓存雪崩需要分散有效期避免集体趴窝。
后面研究如何针对不同场景进行缓存过期时间设置。
更优秀博文补充记录:
Spring Boot + Mybatis + Redis二级缓存(Java Web现代化开发)
总结
Mybatis二级缓存控制粒度太过于简单,无法针对命名空间独立设置过期时间,无法对缓存增加逻辑判断。对于简单的单表查询,快速开发可以使用,对于并发高要求的环境下不推荐,建议自己根据缓存框架编写灵活的代码。