本文主要记录redis作为mybatis二级缓存时所遇到的问题,因为mybatis以及缓存基于sqlsession,会话完成后,缓存数据就会被清空,二级缓存基于mapper,虽然解决了sqlsession的问题,但是还是基于本地内存,应用被杀掉后,缓存还是被清空,并且分布式环境下会出现缓存一致性问题。所以现在大部分二级缓存都是使用redis实现,有效解决分布式情况下缓存问题。当然,redis分布式缓存也会存在一定的问题。后面有时间再记录。
添加redis依赖及配置
springboot下直接添加如下依赖
1
| implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
这里搭建的是redis哨兵模式,在application.yaml中配置
1 2 3 4 5 6 7 8 9 10 11
| spring: redis: password: 123456 sentinel: master: mymaster nodes: 192.168.0.10:26379,192.168.0.10:26380,192.168.0.10:26381 database: 0 timeout: 60s lettuce: pool: enabled: true
|
配置RedisTemplate
RedisTemplate主要需要配置key的序列化和value的序列化,各类json的框架都需要实现RedisSerializer,我们这里使用Spring配置的Jackson2JsonRedisSerializer如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Bean public Jackson2JsonRedisSerializer<Object> redisJsonSerializer() { Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper();
SimpleModule sm = new SimpleModule(); sm.addSerializer(Long.class, ToStringSerializer.instance); sm.addSerializer(Long.TYPE, ToStringSerializer.instance); sm.addSerializer(BaseEnum.class, BaseEnumSerializer.instance); sm.addSerializer(BaseConstEnum.class, ConstEnumSerializer.instance); sm.addDeserializer(BaseEnum.class, BaseEnumDeserializer.instance); sm.addDeserializer(BaseConstEnum.class, ConstEnumDeserializer.instance); objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); objectMapper.registerModule(sm); serializer.setObjectMapper(objectMapper); return serializer; } ```+ 这个配置很重要,如果不明白会造成很多问题,上面的配置中,特别注意activateDefaultTyping,不然redis反序列化时并不会转为实际的对象,而是LinkedHashMap。如下 ```json [ "com.buguagaoshu.redis.model.User", { "name": "1", "age": "11", "message": "牛逼" } ]
|
如果没设置,json串将是这样
1 2 3 4 5
| { "name": "1", "age": "11", "message": "牛逼" }
|
配置完Jackson2JsonRedisSerializer,就需要配置RedisTemplate了,将刚才配置的Jackson2JsonRedisSerializer注入到RedisTemplate中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Primary @Bean public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory, Jackson2JsonRedisSerializer<Object> serializer) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); template.setKeySerializer(stringRedisSerializer); template.setValueSerializer(serializer); template.setHashKeySerializer(stringRedisSerializer); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); template.setEnableTransactionSupport(true); return template; }
|
Jackson2JsonRedisSerializer作为redis value的序列化与反序列工具。
RedisCache实现
mybatis二级缓存需要实现Cache接口,可以参考mybatis PerpetualCache和LruCache缓存实现,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
| package com.mayahx.stcm.common.config;
import com.mayahx.stcm.common.util.ApplicationContextUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomUtils; import org.apache.ibatis.cache.Cache;
import java.security.MessageDigest; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j public class RedisCache implements Cache {
private static final String CHARSET = "utf-8";
private static final String ALGORITHM = "SHA-256";
private static final String CACHE_NAME = "MyBatis:";
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final String id;
private static volatile RedisServiceImpl redisService;
private volatile MessageDigest messageDigest;
private static final int MIN_EXPIRE_MINUTES = 60;
private static final int MAX_EXPIRE_MINUTES = 120;
public RedisCache(String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require an ID"); } this.id = id; }
@Override public String getId() { return id; }
@Override public void putObject(Object key, Object value) { try { String strKey = getKey(key); int expireMinutes = RandomUtils.nextInt(MIN_EXPIRE_MINUTES, MAX_EXPIRE_MINUTES); getRedisService().set(strKey, value, expireMinutes, TimeUnit.MINUTES); log.info("Put cache to redis, id={}", id); } catch (Exception e) { log.error("Redis put failed, id=" + id, e); } }
@Override public Object getObject(Object key) { try { String strKey = getKey(key); log.info("Get cache from redis, id={}", id); return getRedisService().get(strKey); } catch (Exception e) { log.error("Redis get failed, fail over to db", e); return null; } }
@Override public Object removeObject(Object key) { try { String strKey = getKey(key); getRedisService().delete(strKey); log.info("Remove cache from redis, id={}", id); } catch (Exception e) { log.error("Redis remove failed", e); } return null; }
@Override public void clear() { try { log.info("clear cache, id={}", id); String hsKey = CACHE_NAME + id; Map<Object, Object> idMap = getRedisService().hashEntries(hsKey); if (!idMap.isEmpty()) { Set<Object> keySet = idMap.keySet(); Set<String> keys = new HashSet<>(keySet.size()); keySet.forEach(item -> keys.add(item.toString())); getRedisService().delete(keys); getRedisService().delete(hsKey); } } catch (Exception e) { log.error("clear cache failed", e); } }
@Override public int getSize() { return 0; }
@Override public ReadWriteLock getReadWriteLock() { return readWriteLock; }
private String getKey(Object cacheKey) { String cacheKeyStr = cacheKey.toString(); log.info("count hash key, cache key origin string:{}", cacheKeyStr); String strKey = byte2hex(getSHADigest(cacheKeyStr)); log.info("hash key:{}", strKey); String key = CACHE_NAME + strKey; getRedisService().hashSet(CACHE_NAME + id, key, "1"); return key; }
private byte[] getSHADigest(String data) { try { if (messageDigest == null) { synchronized (MessageDigest.class) { if (messageDigest == null) { messageDigest = MessageDigest.getInstance(ALGORITHM); } } } return messageDigest.digest(data.getBytes(CHARSET)); } catch (Exception e) { log.error("SHA-256 digest error: ", e); throw new SecurityException("SHA-256 digest error"+"id=" + id + "."); } }
private String byte2hex(byte[] bytes) { StringBuilder sign = new StringBuilder(); for (byte aByte : bytes) { String hex = Integer.toHexString(aByte & 0xFF); if (hex.length() == 1) { sign.append("0"); } sign.append(hex.toUpperCase()); } return sign.toString(); }
private RedisService getRedisService() { if (redisService == null) { synchronized (RedisService.class) { if (redisService == null) { redisService = ApplicationContextUtil.getBean(RedisServiceImpl.class); } } } return redisService; } }
|
cache接口实现后,我们需要告诉mybatis,我们使用redis二级缓存。
首先有一个全局缓存开关配置
1 2 3 4 5 6 7 8 9 10 11
| mybatis-plus: type-enums-package: com.mayahx.stcm.api.entity.diagnose.enums global-config: db-config: logic-delete-value: 'NULL' logic-not-delete-value: 1 mapper-locations: classpath*:mapper/*Mapper.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler cache-enabled: true
|
第二步需要在mapper xml中声明缓存引用
1
| <cache-ref namespace="com.mayahx.stcm.diagnose.mapper.CsMapper"/>
|
最后一步,再mapper接口上添加@CacheNamespace注解
1 2 3
| @Mapper @CacheNamespace(implementation = RedisCache.class,eviction = RedisCache.class) public interface CsMapper extends BaseMapper<Cs> ...
|
测试缓存生效
请求一个GET接口,看看是否是第一次走DB,第二次走redis缓存

看看运行日志

我们再次请求接口
