背景:
最近公司项目中需要引入缓存机制来减轻数据库负载,所以对一些缓存方案进行了研究,其中包括看了几篇讲mybatis的二级缓存的,写的都很不错,推荐美团的一篇:聊聊MyBatis缓存机制 对mybatis的缓存机制讲的很清楚了。博主在本文提供一种使用redis的hash结构来实现mybatis的二级缓存方案,初次尝试,如有疑问欢迎指正。
环境:
jdk 1.8
ide:Intellij 2017.1
spring-boot :2.0.4.RELEASE
redis:4.0.2
mysql:5.7.20
搭建项目&实现
通过idea新建spring-boot项目,傻瓜式的选择一些需要的starter即可,不再赘述:参考图片
数据源、redis、mybatis相关配置 (spring-boot的自动配置这里也很简单)直接加入相关的配置即可如下:
spring:
redis:
host: 127.0.0.1
password:
port: 6379
jedis:
pool:
min-idle: 0
max-active: 8
max-idle: 8
max-wait: 5000
datasource:
url: jdbc:mysql://127.0.0.1:3306/song?characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
mybatis:
configuration:
cache-enabled: true #开启全局二级缓存 mybatis通过这个会使用CachingExecutor
mapper-locations: classpath:mapper/*.xml
正常mybatis使用配置(beans、mapper配置、mapper接口):
定义一个User bean
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private Integer gender;
}
mapper接口
@Mapper
public interface UserMapper {
User selectOne(Integer id);
void insertOne(User user);
}
mapper配置
<?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.UserMapper">
<!--使用自定义的缓存
<cache type="com.example.demo.cache.RedisCache"/>
-->
<resultMap id="userResultMap" type="com.example.demo.entities.User">
<id property="id" column="id" jdbcType="INTEGER" javaType="java.lang.Integer"/>
<result property="name" column="name" jdbcType="VARCHAR" javaType="java.lang.String"/>
<result property="gender" column="gender" jdbcType="TINYINT" javaType="java.lang.Integer"/>
</resultMap>
<select id="selectOne" resultMap="userResultMap" parameterType="java.lang.Integer">
SELECT id, name, gender
FROM `USER`
WHERE id = #{id}
</select>
<insert id="insertOne" parameterType="com.example.demo.entities.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `USER` (name, gender) VALUES (#{name}, #{gender})
</insert>
</mapper>
最好先测试下不使用缓存时代码能用
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);
}
}
自定义实现mybatis的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不能通过spring的resource注入,因为cache对象是和id绑定的,可以debug看下这个id其实是mapper中的namespace、这也是为什么自定义cache中必须有一个有参构造器;这个实现中也是根据这个id来为每个namespace对应一个redis的hash结构,在clear时只删掉本namespace的缓存即可
mapper中使用自定义cache,只需要在mapper中加上<cache>标签,具体其中的属性可以自己Google
<cache type="com.example.demo.cache.RedisCache"/>
所有代码见:https://github.com/swg-liuge/spring-boot-mybatis-cache2-redis
原文链接:
#############################
SpringBoot使用Redis做缓存
随着时间的积累,应用的使用用户不断增加,数据规模也越来越大,往往数据库查询操作会成为影响用户使用体验的瓶颈,此时使用缓存往往是解决这一问题非常好的手段之一。Spring 3开始提供了强大的基于注解的缓存支持,可以通过注解配置方式低侵入的给原有Spring应用增加缓存功能,提高数据访问性能。
在Spring Boot中对于缓存的支持,提供了一系列的自动化配置,使我们可以非常方便的使用缓存。
缓存工具简单比较
实现缓存有多种方式:EhCache、MemCached、Redis等
EhCache:纯Java进程内缓存框架,配置简单、结构清晰、功能强大。开始接触javaweb的时候,不管是使用hibernate还是mybatis,这个应该是最常见的缓存的工具;经常会用作二级缓存。百度百科上的特点:
MemCached:一个自由开源的,高性能的分布式对象缓存;使用key-value存储系统。特点:
协议简单
基于libevent的事件处理
内置内存存储方式
memcached不互相通信的分布式
Redis:NoSql数据库,不仅仅支持k/v存储,同时还提供list、set、hash的数据结构的存储。具体学习材料可以进官网,传送门:http://www.redis.cn/
Redis缓存介绍
本文的项目主要是SpringBoot+mybatis+redis+mysql。使用redis做缓存可以有几种实现方式:
直接将redis作为mybatis的二级缓存;
通过注解方式给项目添加缓存;
手动调用jedis。这种方式不推荐。。。
环境的搭建
在SpringBoot中已经有对于缓存的支持,只需要做简单的配置添加即可。
pom.xml文件
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
添加redis的配置,这列使用的是yml的配置
# redis配置
redis:
#数据库索引,默认为0
database: 0
#服务器地址
host: localhost
#端口
port: 6379
#密码(默认为空)
password:
pool:
#连接池最大连接数(使用负值表示没有限制)
max-active: 8
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 5000
使用
使用注解配置缓存
这种方式比较简单,在相应的service中添加Cache注解就可以,项目代码,mybatis的配置文件以及mapper文件省略,
test实体类:这里使用了lombok的@Data注解
@Data
public class Test implements Serializable{
private String id;
private String name;
private String remake;
}
service类:这里的注解使用就是在service类中使用
@Service
@CacheConfig(cacheNames = "test")
public class TestService {
@Autowired
private TestMapper testMapper;
@Cacheable(key = "#p0")
public Test get(String id) {
return testMapper.get(id);
}
@CachePut(key = "#p0.id")
public Test insert(Test test) {
testMapper.insert(test);
return test;
}
@CachePut(key = "#p0.id")
public Test update(Test test) {
testMapper.update(test);
return test;
}
}
测试类:
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestServiceTest {
private static final Logger logger = LoggerFactory.getLogger(TestServiceTest.class);
@Autowired
private TestService testService;
@Test
public void get() {
com.wqh.blog.domain.Test test = testService.get("d8e875c8-9425-485b-9665-f5dda1e788bf");
logger.info("======"+test+"=============");
}
@Test
public void insert() {
com.wqh.blog.domain.Test test = new com.wqh.blog.domain.Test();
test.setId(UUID.randomUUID().toString());
test.setName("redis");
test.setRemake("hhhhhhhhhhhhhhhhhhhhhhh");
testService.insert(test);
}
@Test
public void update() {
}
}
首先运行,插入方法,然后使用RedisClient查看,可以发现已经有数据。然后调用get方法,控制台并没有打印sql语句,配置成功
CACHE注解
这里直接引用程序员DD大神博客的内容:
@CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = “test”):配置了该数据访问对象中返回的内容将存储于名为test的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义。
@Cacheable:配置了get函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:
value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了
key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = “#p0”):使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档
condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = “#p0”, condition = “#p0.length() < 3”),表示只有当第一个参数的长度小于3的时候才会被缓存,若做此配置上面的AAA用户就不会被缓存,读者可自行实验尝试。
unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的
cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
@CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析
@CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:
allEntries:非必需,默认为false。当为true时,会移除所有数据
beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
使用redis做mybatis的二级缓存
在mybatis中有一级缓存、二级缓存
- 一级缓存:该缓存是基于SqlSession的,mybatis默认开启一级缓存。
- 二级缓存:该缓存是Mapper级别的,默认没有开启二级缓存,需要在配置文件中开启:
<!-- 开启二级缓存,默认是false -->
<setting name="cacheEnabled" value="true"/>
依赖跟前面一样,使用redis做二级缓存,主要是要实现org.apache.ibatis.cache包下的Cache接口
public class RedisCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final String id;
private RedisTemplate redisTemplate;
/**
* redis过期时间
*/
private static final long EXPIRE_TIME_IN_MINUTES = 300;
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return id;
}
/**
* Put query result to redis
* TimeUnit.HOURS 设置时间的类型:时、分、秒、毫秒
* @param key
* @param value
*/
@Override
public void putObject(Object key, Object value) {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.HOURS);
logger.debug("Put query result to redis");
}
/**
* Get cached query result from redis
*
* @param key
* @return
*/
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
logger.debug("Get cached query result from redis");
Object o = opsForValue.get(key);
return o;
}
/**
* Remove cached query result from redis
*
* @param key
* @return
*/
@Override
@SuppressWarnings("unchecked")
public Object removeObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(key);
logger.debug("Remove cached query result from redis");
return null;
}
/**
* Clears this cache instance
*/
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
return null;
});
logger.debug("Clear all the cached query result from redis");
}
/**
* This method is not used
*
* @return
*/
@Override
public int getSize() {
return 0;
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = SpringContextHolder.getBean("cacheRedisTemplate");
}
return redisTemplate;
}
}
自定义一个序列化接口
public class RedisObjectSerializer implements RedisSerializer<Object> {
private Converter<Object, byte[]> serializer = new SerializingConverter();
private Converter<byte[], Object> deserializer = new DeserializingConverter();
static final byte[] EMPTY_ARRAY = new byte[0];
@Override
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
return EMPTY_ARRAY;
}
try {
return serializer.convert(object);
} catch (Exception ex) {
return EMPTY_ARRAY;
}
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if (isEmpty(bytes)) {
return null;
}
try {
return deserializer.convert(bytes);
} catch (Exception ex) {
throw new SerializationException("Cannot deserialize", ex);
}
}
private boolean isEmpty(byte[] data) {
return (data == null || data.length == 0);
}
}
redis的配置类
@Configuration
public class RedisConfig {
@Bean
public JedisConnectionFactory jedisConnectionFactory(){
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate<String,Object> cacheRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 设置值(value)的序列化采用自定义的RedisObjectSerializer
redisTemplate.setValueSerializer(new RedisObjectSerializer());
// 设置键(key)的序列化采用jackson2JsonRedisSerializer
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
前面已经开启了二级缓存,现在需要修改Mapper.xml文件
<!--表示开启基于redis的二级缓存-->
<cache type="com.wqh.blog.config.cache.RedisCache">
<property name="eviction" value="LRU" />
<property name="flushInterval" value="6000000" />
<property name="size" value="1024" />
<property name="readOnly" value="false" />
</cache>
另外在插入、修改和删除时需要添加flushCache="true"。
缓存添加成功的话,使用RedisClient可以查看:
手动将数据添加到redis
在上面的RedisConfig类中添加方法,比如我这里要将Test对象保存到redis:
@Bean
public RedisTemplate<String,Test> testRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Test> template = new RedisTemplate<String,Test>();
template.setConnectionFactory(redisConnectionFactory);
//直接使用Jedis提供的StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
Jackson2JsonRedisSerializer<Test> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Test>(Test.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//使用jackson2JsonRedisSerializer序列化value
template.setValueSerializer(jackson2JsonRedisSerializer);
return template;
}
测试类
@Autowired
private RedisTemplate<String, com.wqh.blog.domain.Test> testRedisTemplate;
@Test
public void testRedis() throws Exception{
com.wqh.blog.domain.Test test = new com.wqh.blog.domain.Test();
test.setId(UUID.randomUUID().toString());
test.setName("redis");
test.setRemake("hhhhhhhhhhhhhhhhhhhhhhh");
testRedisTemplate.opsForValue().set(test.getId(),test);
Assert.assertEquals("redis",testRedisTemplate.opsForValue().get(test.getId()).getName());
}
问题
前面使用注解方式添加缓存的功能,如果使用RedisClient查看数据的话,会发现数出现乱码现象,这主要是序列化问题。解决办法只要自定义一个redisTemplate的bean
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(jedisConnectionFactory());
//key序列化方式,但是如果方法上有Long等非String类型的话,会报类型转换错误
//Long类型不可以会出现异常信息;
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(redisSerializer);
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);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
转自:
http://www.wanqhblog.top/2017/12/26/SpringBoot%E4%BD%BF%E7%94%A8Redis%E5%81%9A%E7%BC%93%E5%AD%98/
####################################
最近项目需要针对mybatis查询加入缓存,使用redis,于是上网查找mybatis缓存 redis实现的相关文章,有很多关于mybatis redis缓存的介绍以及mybatis Cache接口的redis实现代码,但内容都是一致,看得出都转载某一人的手笔,虽然提供的代码逻辑是正确的,但是在项目应用中都存在问题。请小心使用! 为什么网上流传的mybatis redis实现代码有问题,只要你调试过、测试过,你会发现有这么个问题存在:当页面对数据表存在CUD操作时,以往所有的查询缓存都会clear掉,当再次查询时,重新缓存;只有任意一个CUD操作,整个mybatis缓存都会清空,这样会带来相反的结果,不能利用缓存提供效率,反而降低了系统性能。我们的目标就是,针对Mybatis的Mapper.xml文件,当数据变动后,只清除变动语句存在mapper.xml里的查询缓存,而不是清除所有mapper.xml查询语句缓存。 想了解这块的人,可以对比本文实现代码,和网上其他文章的实现代码,虽然缓存都是可用,但是对系统性能却存在极大差别。
1,开启mybatis二级缓存,true ,在sqlSessionFactory配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 配置sqlSessionFactory的参数 -->
<property name="configurationProperties">
<props>
<prop key="cacheEnabled">true</prop>
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<prop key="lazyLoadingEnabled">false</prop>
<!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指定),不会加载关联表的所有字段,以提高性能 -->
<prop key="aggressiveLazyLoading">true</prop>
<!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
<prop key="multipleResultSetsEnabled">true</prop>
<!-- 允许使用列标签代替列名 -->
<prop key="useColumnLabel">true</prop>
<!-- 允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值),数据表的PK生成策略将被覆盖 -->
<prop key="useGeneratedKeys">true</prop>
<!-- 给予被嵌套的resultMap以字段-属性的映射支持 -->
<prop key="autoMappingBehavior">FULL</prop>
<!-- 对于批量更新操作缓存SQL以提高性能 -->
<prop key="defaultExecutorType">BATCH</prop>
<!-- 数据库超过25000秒仍未响应则超时 -->
<prop key="defaultStatementTimeout">25000</prop>
</props>
</property>
</bean>
复制代码
2,在需要添加缓存的mybatis mapper.xml文件中指定缓存Cache实现类,广泛采用LRU算法:
<mapper namespace="com......Mapper">
<cache eviction="LRU" type="com.xfx.service.cache.redis.MybatisRedisCache" />
</mapper>
复制代码
3,mybatis redis缓存重点,Cache实现类(通过jedisPool获取jedis的方式你可以修改):
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.ibatis.cache.Cache;
import org.apache.log4j.Logger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/*
* 使用第三方缓存服务器,处理二级缓存
* <a href="http://www.zyiqibook.com">在一起 学习交流分享网 功能源码分享</a>
* @author xfxpeter@gmail.com
*/
public class MybatisRedisCache implements Cache {
private static final Logger logger = Logger.getLogger(MybatisRedisCache.class);
/** The ReadWriteLock. */
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private JedisPool jedisPool;
private static final int DB_INDEX = 1;
private final String COMMON_CACHE_KEY = "COM:";
private static final String UTF_8 = "utf-8";
/**
* 按照一定规则标识key
*/
private String getKey(Object key) {
StringBuilder accum = new StringBuilder();
accum.append(COMMON_CACHE_KEY);
accum.append(this.id).append(":");
accum.append(DigestUtils.md5Hex(String.valueOf(key)));
return accum.toString();
}
/**
* redis key规则前缀
*/
private String getKeys() {
return COMMON_CACHE_KEY + this.id + ":*";
}
private String id;
private Properties properties;
{
properties = getProp();
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(Integer.valueOf(properties
.getProperty("redis.pool.maxIdle")));
jedisPool = new JedisPool(config, properties.getProperty("redis.host"),
Integer.valueOf(properties.getProperty("redis.port")),
Integer.valueOf(properties.getProperty("redis.timeout")),
properties.getProperty("redis.password"));
}
/**
* 加载项目redis连接属性文件
*/
private Properties getProp(){
if(properties == null || properties.isEmpty()){
String propName = "config.properties";
properties = new Properties();
InputStream is = null;
BufferedReader bf = null;
try {
is= this.getClass().getResourceAsStream("/META-INF/config.properties");//将地址加在到文件输入流中
bf = new BufferedReader(new InputStreamReader(is,"UTF-8"));//转为字符流,设置编码为UTF-8防止出现乱码
properties.load(bf);//properties对象加载文件输入流
} catch (UnsupportedEncodingException e) {
logger.error(propName + "编码格式转换失败,不支持指定编码。" + e);
} catch (FileNotFoundException e) {
logger.error(propName + "属性文件common.properties不存在。" + e);
} catch (IOException e) {
logger.error(propName + "属性文件common.properties读取失败。" + e);
} catch (Exception e) {
logger.error(propName + "属性文件common.properties读取失败。" + e);
} finally {
try {//文件流关闭
if(bf != null){
bf.close();
}
if(is != null ){
is.close();
}
} catch (IOException e) {
logger.error("关闭文件流失败。" + e);
}
}
}
return properties;
}
public MybatisRedisCache() {
}
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("必须传入ID");
}
logger.debug("MybatisRedisCache:id=" + id);
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public int getSize() {
Jedis jedis = null;
int result = 0;
boolean borrowOrOprSuccess = true;
try {
jedis = jedisPool.getResource();
jedis.select(DB_INDEX);
Set<byte[]> keys = jedis.keys(getKeys().getBytes(UTF_8));
if (null != keys && !keys.isEmpty()) {
result = keys.size();
}
logger.debug(this.id+"---->>>>总缓存数:" + result);
} catch (Exception e) {
borrowOrOprSuccess = false;
if (jedis != null)
jedisPool.returnBrokenResource(jedis);
} finally {
if (borrowOrOprSuccess)
jedisPool.returnResource(jedis);
}
return result;
}
@Override
public void putObject(Object key, Object value) {
Jedis jedis = null;
boolean borrowOrOprSuccess = true;
try {
jedis = jedisPool.getResource();
jedis.select(DB_INDEX);
byte[] keys = getKey(key).getBytes(UTF_8);
jedis.set(keys, SerializeUtil.serialize(value));
logger.debug("添加缓存--------"+this.id);
//getSize();
} catch (Exception e) {
borrowOrOprSuccess = false;
if (jedis != null)
jedisPool.returnBrokenResource(jedis);
} finally {
if (borrowOrOprSuccess)
jedisPool.returnResource(jedis);
}
}
@Override
public Object getObject(Object key) {
Jedis jedis = null;
Object value = null;
boolean borrowOrOprSuccess = true;
try {
jedis = jedisPool.getResource();
jedis.select(DB_INDEX);
value = SerializeUtil.unserialize(jedis.get(getKey(key).getBytes(UTF_8)));
logger.debug("从缓存中获取-----"+this.id);
//getSize();
} catch (Exception e) {
borrowOrOprSuccess = false;
if (jedis != null)
jedisPool.returnBrokenResource(jedis);
} finally {
if (borrowOrOprSuccess)
jedisPool.returnResource(jedis);
}
return value;
}
@Override
public Object removeObject(Object key) {
Jedis jedis = null;
Object value = null;
boolean borrowOrOprSuccess = true;
try {
jedis = jedisPool.getResource();
jedis.select(DB_INDEX);
value = jedis.del(getKey(key).getBytes(UTF_8));
logger.debug("LRU算法从缓存中移除-----"+this.id);
//getSize();
} catch (Exception e) {
borrowOrOprSuccess = false;
if (jedis != null)
jedisPool.returnBrokenResource(jedis);
} finally {
if (borrowOrOprSuccess)
jedisPool.returnResource(jedis);
}
return value;
}
@Override
public void clear() {
Jedis jedis = null;
boolean borrowOrOprSuccess = true;
try {
jedis = jedisPool.getResource();
jedis.select(DB_INDEX);
Set<byte[]> keys = jedis.keys(getKeys().getBytes(UTF_8));
logger.debug("出现CUD操作,清空对应Mapper缓存======>"+keys.size());
for (byte[] key : keys) {
jedis.del(key);
}
//下面是网上流传的方法,极大的降低系统性能,没起到加入缓存应有的作用,这是不可取的。
//jedis.flushDB();
//jedis.flushAll();
} catch (Exception e) {
borrowOrOprSuccess = false;
if (jedis != null)
jedisPool.returnBrokenResource(jedis);
} finally {
if (borrowOrOprSuccess)
jedisPool.returnResource(jedis);
}
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}
我的疑问:只能这样解析key吗,不能自定义生成key的规则吗?
SerializeUtil.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* 序列化与反序列化
* <a href="http://www.zyiqibook.com">在一起 学习交流分享网 功能源码分享</a>
* @author xfxpeter@gmail.com
*/
public class SerializeUtil {
public static byte[] serialize(Object object) {
if (null == object) return null;
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
//序列化
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static Object unserialize(byte[] bytes) {
if(null == bytes) return null;
ByteArrayInputStream bais = null;
try {
//反序列化
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
复制代码
redis缓存使用好了,对项目性能有极大改变,特别是大型web应用项目,这是大型项目性能优化的重点内容。
#############################################################
spring boot 2.1.3
为了减少数据库访问频率,提高查询速度,今天讲一下spring boot mybatis 怎么集成 redis缓存。
第一步:添加redis相关jar包依赖
这里我只贴出集成redis缓存所需要的jar包,至于链接数据库,和基本的项目启动请参考
Spring boot 2 mybatis配置
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
至于jar包spring-boot-starter-cache
至少在版本2.1.3可以不用引用,2.1.3版本jar包spring-boot-starter-data-redis
已经自带cache所需要的jar包,不用重复引用。
第二步:添加redis链接数据库配置文件
spring:
# 数据库连接
#datasource:
# password: 111111
# url: jdbc:mysql://120.77.241.43:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8
# username: root
# redis
redis:
host: 127.0.0.1
port: 6300
password: 123
# Redis数据库索引(默认为0)
database: 0
#连接超时时间(毫秒)
timeout: 10000
jedis: #一些常规配置
pool:
# 连接池中的最大空闲连接
max-idle: 60
# 连接池中的最小空闲连接
min-idle: 30
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 60000
# 连接池最大连接数(使用负值表示没有限制)
max-active: 200
第三步:添加RedisConfig配置类
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean(name = "cacheKeyGenerator")
@Primary//@Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常
public KeyGenerator keyGenerator() {
return (target, method, params) -> CacheHashCode.of(params);
}
/**
* spring boot 缓存默认配置
* @param factory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
//默认缓存时间
.cacheDefaults(getRedisCacheConfigurationWithTtl(60))
.transactionAware()
//自定义缓存时间
.withInitialCacheConfigurations(getRedisCacheConfigurationMap())
.build();
}
/**
* 自定义缓存时间
* @return
*/
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
redisCacheConfigurationMap.put("test", this.getRedisCacheConfigurationWithTtl(3000));
return redisCacheConfigurationMap;
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(seconds))//定义默认的cache time-to-live.(缓存存储有效时间)
.disableCachingNullValues()//静止缓存为空
//此处定义了cache key的前缀, 避免公司不同项目之间的key名称冲突.
.computePrefixWith(cacheName -> "api".concat(":").concat(cacheName).concat(":"))
//定义key和value的序列化协议, 同时的hash key和hash value也被定义.
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(createJackson2JsonRedisSerializer()))
//自定义key的生成策略, 将方法参数转换为hashcode, 作为redis key. 需要做两个事情, 一个是添加一个自定义的ConversionService, 另一个是需要自定义一个KeyGenerator.
.withConversionService(new CacheKeyConversionService());
}
/**
* 创建redisTemplate工具
* @param factory
* @return
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
template.setKeySerializer(new StringRedisSerializer());//设置key序列化类,否则key前面会多了一些乱码
template.setValueSerializer(createJackson2JsonRedisSerializer());//设置value序列化
template.setHashKeySerializer(createJackson2JsonRedisSerializer());//设置 hash key 序列化
template.setHashValueSerializer(createJackson2JsonRedisSerializer());//设置 hash value 序列化
template.setEnableTransactionSupport(true);//设置redis支持数据库的事务
template.afterPropertiesSet();//初始化设置并且生效
return template;
}
/**
* 创建redis序列化
* @return
*/
private RedisSerializer<Object> createJackson2JsonRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}
public class CacheKeyConversionService implements ConversionService {
@Override
public boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType) {
return true;
}
@Override
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
return true;
}
@Nullable
@Override
public <T> T convert(@Nullable Object source, Class<T> targetType) {
return (T) convert(source);
}
@Nullable
@Override
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
return convert(source);
}
private Object convert(Object source) {
if (source instanceof CacheHashCode) {
return ((CacheHashCode) source).hashString();
}
return CacheHashCode.of(source).hashString();
}
}
第四步:在需要存入redis缓存的方法上添加注解
注解介绍
- @Cacheable
获取缓存 如果有缓存 直接返回
属性 | 类型 | 功能 |
value | String[] | 缓存的名称 和cacheNames功能一样 |
cacheNames | String[] | 缓存的名称和value功能一样 |
key | String | 缓存key的值、默认是以所有的参数作为key、也可以直接配置keyGenerator |
keyGenerator | String | 缓存key的生成器 |
cacheManager | String | 配置使用那个缓存管理器、和cacheResolver排斥 |
cacheResolver | String | 定义使用那个拦截器、和cacheManager互斥 |
condition | String | 根据spel表达式来可以配置什么条件下进行缓存 默认全部缓存 |
unless | String | 和condition相反 |
sync | boolean | 是否开启同步功能、默认不开启 |
- @CachePut
执行并且更新缓存相关 不管如何 肯定会执行方法 然后返回 这样可以更新缓存的内容
属性 | 类型 | 功能 |
value | String[] | 缓存的名称 和cacheNames功能一样 |
cacheNames | String[] | 缓存的名称和value功能一样 |
key | String | 缓存key的值、默认是以所有的参数作为key、也可以直接配置keyGenerator |
keyGenerator | String | 缓存key的生成器 |
cacheManager | String | 配置使用那个缓存管理器、和cacheResolver排斥 |
cacheResolver | String | 定义使用那个拦截器、和cacheManager互斥 |
condition | String | 根据spel表达式来可以配置什么条件下进行缓存 默认全部缓存 |
unless | String | 和condition相反 |
- @CacheEvict
删除缓存相关
属性 | 类型 | 功能 |
value | String[] | 缓存的名称 和cacheNames功能一样 |
cacheNames | String[] | 缓存的名称和value功能一样 |
key | String | 缓存key的值、默认是以所有的参数作为key、也可以直接配置keyGenerator |
keyGenerator | String | 缓存key的生成器 |
cacheManager | String | 配置使用那个缓存管理器、和cacheResolver排斥 |
cacheResolver | String | 定义使用那个拦截器、和cacheManager互斥 |
condition | String | 根据spel表达式来可以配置什么条件下进行缓存 默认全部缓存 |
allEntries | boolean | 是否删除所有键的缓存 默认不删除 |
beforeInvocation | boolean | 是否在调用此方法前 删除缓存 |
- @CacheConfig
在类级别统一的配置缓存公共配置
属性 | 类型 | 功能 |
cacheNames | String[] | 缓存的名称和value功能一样 |
keyGenerator | String | 缓存key的生成器 |
cacheManager | String | 配置使用那个缓存管理器、和cacheResolver排斥 |
cacheResolver | String | 定义使用那个拦截器、和cacheManager互斥 |
- @EnableCaching
开启缓存以及缓存的全局配置
属性 | 类型 | 功能 |
proxyTargetClass | boolean | 是否要基于cglib生成代理去实现缓存 |
mode | AdviceMode | 配置那种模式去实现缓存、默认是AdviceMode.PROXY 可以切换为 AdviceMode#ASPECTJ |
order | int | 设置缓存管理器执行的顺序 |
- @Caching
对多个缓存组的配置
属性 | 类型 | 功能 |
cacheable | Cacheable | 配置获取缓存相关的配置 |
put | CachePut | 配置如何更新缓存的相关配置 |
evict | CacheEvict | 配置如何删除缓存的相关配置 |
第五步:在启动类配置开启缓存
第六步:运行查看效果
controller
查看redis客户端存储的数据