springboot整合redis
适合当如缓存场景:
- 即时性、数据一致性要求不高的。
- 访问量大且更新频率不高的数据(读多,写少)
承担Persistence工作。
读模式缓存使用流程:

凡是放入Cache中的Data,我们应该指定过期时间,使其可以再系统即使没有主动更新Data也能自动触发Data加载进Cache的流程。避免业务崩溃导致的Data永久不一致Problem。
解决Distributed Cache中本地Cache导致Data不一致Problem,可以Usageredis中间件解决。
springboot整合redis
1、引入springboot整合的start:pomFile引入Dependency
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
引入Dependency之后就会有RedisAutoConfiguration,里面可以看的到redis的Configuration FileRedis.Properties.
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
Redis.PropertiesFile里面包含了redis的Configuration信息:
@ConfigurationProperties(
prefix = "spring.redis"
)
public class RedisProperties {
private int database = 0;
private String url;
private String host = "localhost";
private String username;
private String password;
private int port = 6379;
private boolean ssl;
private Duration timeout;
private Duration connectTimeout;
private String clientName;
private RedisProperties.ClientType clientType;
private RedisProperties.Sentinel sentinel;
private RedisProperties.Cluster cluster;
private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
2、redisConfiguration,可以根据上面的Redis.PropertiesType,再applicatioin.yml中Configuration相关属性。
spring:
redis:
host: 127.0.0.1
port: 6379
上面一个最简单的redisConfiguration就完成了。
3、Usageredis
由上面的RedisAutoConfiguration自动Configuration类,可以看到自动装配了RedisTemplate<Object, Object>、StringRedisTemplate。
RedisTemplate<Object, Object>:一般是字符串的Key,和序列化后的字符串value。
由于UsageStringType的key、value较多,所以还提供了StringRedisTemplate。
public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
this.setKeySerializer(RedisSerializer.string());
this.setValueSerializer(RedisSerializer.string());
this.setHashKeySerializer(RedisSerializer.string());
this.setHashValueSerializer(RedisSerializer.string());
}
public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
this();
this.setConnectionFactory(connectionFactory);
this.afterPropertiesSet();
}
protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
return new DefaultStringRedisConnection(connection);
}
}
StringRedisTemplate也是继承了RedisTemplate<String, String>,都是字符串的key,value.
this.setKeySerializer(RedisSerializer.string()); this.setValueSerializer(RedisSerializer.string()); this.setHashKeySerializer(RedisSerializer.string()); this.setHashValueSerializer(RedisSerializer.string());
上面的key、value都是用RedisSerializer.string()Type序列化。
UsageConfiguration好的StringRedisTemplate。
StringRedisTemplate的value由多种不同Type的值。

保存Get值:
@Test
public void testStringRedisTemplate(){
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
opsForValue.set("hello", "world"+ UUID.randomUUID().toString());
String hello = opsForValue.get("hello");
System.out.println("之前保存值为:"+hello);
}
输出结果:
之前保存值为:world1e9f395e-67b8-4077-9912-c690c7da0f06
redisDatabase的值:

注意保存Get时,key必须是一样的。
value一般是序列化后的json字符串,因为json字符串是跨语言跨平台的。vlaue存复杂对象时,序列化可以用alibaba的fastjson。
Map<String,List<UserEntity>> data:要存入redis的复杂数据,通过序列化后存在redis。
String s = JSON.toJSONString(data);
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("mydata",s);
同样Get的jsonData也需要反序列化后才能Usage:比如转化成一个复杂Data格式
String jsonStr= ops.get("mydata");
Map<String,List<UserEntity>> result = JSON.parseObject(jsonStr, new TypeReference<Map<String,List<UserEntity>>>() {});
注意:
springboot2.0后默认Usagelettuce作为操作redis的客户端,Usagenetty进行网络通信。但是lettuce的bug导致netty容易堆外内存溢出。netty如果没有指定堆外内存,默认Usage-Xmx300m。可以通过-Dio.netty.maxDirectMemorySettings堆外内存大小,但是始终会出现堆外内存溢出。
private static void incrementMemoryCounter(int capacity) {
if (DIRECT_MEMORY_COUNTER != null) {
long newUsedMemory = DIRECT_MEMORY_COUNTER.addAndGet((long)capacity);
if (newUsedMemory > DIRECT_MEMORY_LIMIT) {
DIRECT_MEMORY_COUNTER.addAndGet((long)(-capacity));
throw new OutOfDirectMemoryError("failed to allocate " + capacity + " byte(s) of direct memory (used: " + (newUsedMemory - (long)capacity) + ", max: " + DIRECT_MEMORY_LIMIT + ')');
}
}
}
Solution:不能只用-Dio.netty.maxDirectMemory去调大内存。
1、升级netty客户端。
2、切换Usagejedis
如何使用jedis

redis默认Usage的是lettuce,因此需要排除掉lettuce,引入jedis。
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion><!--排除lettuce-->
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
lettuce、jedis都是操作redis的底层客户端。RedisTemplate是对lettuce、jedis对redis操作的再次封装,以后操作都可以用RedisTemplate进行操作redis。
通过redis的自动装配可以看到是引入了lettuce,jedis的。
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
通过jedis可以看到。
class JedisConnectionConfiguration extends RedisConnectionConfiguration {
JedisConnectionConfiguration(RedisProperties properties, ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration, ObjectProvider<RedisClusterConfiguration> clusterConfiguration) {
super(properties, sentinelConfiguration, clusterConfiguration);
}
@Bean
JedisConnectionFactory redisConnectionFactory(ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
return this.createJedisConnectionFactory(builderCustomizers);
}
无论是lettuce、jedis最后都会注入JedisConnectionFactory,就是redis要用的。
高并发下缓存失效问题-缓存穿透:
Cache穿透:
指查询一个一定不存的Data,由于Cache是不命中,将去查询Database,但是Database也没有此记录,我们将这此查询的null写入Cache,这将导致这个不存在的Data每次请求都要到Storage层查询,失去了Cache的意义。
风险:利用不存在的Data进行攻击,,Database瞬时压力增大,最终导致崩溃。
Solution:null结果也Cache到redis,并加入短暂的过期时间。
高并发下缓存失效-缓存雪崩
Cache Avalanche:指在我们SettingsCache时key采用了相同的过期时间,导致Cache在某一时间同时失效,请求全部转发到DB,DB瞬时压力过大雪崩。
Solution:原有的失效时间Basic上增加一个随机值,比如11-5分分钟随机,这样每一个Cache的过期时间重复概率就会降低,很难引发集体失效。
高并发下缓存失效问题-缓存击穿
Cache穿透:
对于一些Settings了过期时间的key,如果这些key可能会在某些时间点被超高Concurrency地访问,是一种费用“热点”的Data。
如果这个key在大量请求同时进来前正好失效,那么所有对这个key的Data查询都落到DB上,我们称为Cache Penetration。
Solution:
加Lock
大量Concurrency只让一个去查,其他人等待,查到以后释放Lock,其它人Get到Lock,先差Cache,就会有Data了,而不用去DB查询。
Summary:
1、解决Cache穿透:空结果也Cache。
2、解决Cache Avalanche:Settings过期时间(随机值)。
3、解决Cache Penetration:加Lock。
例如:
ops.set("RecordData",null, 1, TimeUnit.DAYS); //null值也缓存
ops.set("RecordData",JSON.toJSONString(recordEntity), 1, TimeUnit.DAYS);
解Lock:
1、通过synchronized/Lock本地Lock。
2、Distributed LockSolutionRedisson。
但是本地Locksynchronized/Lock不能解决Distributed带来的击穿Problem。因此Distributed还得用Redisson进行加Lock解决。
redissonDistributed Lock,可参考另一篇redissonDistributed Lock。
//本地加锁
public RecordEntity getEntityByIdByRedis(Long id){
synchronized (this){
ValueOperations<String, String> ops = redisTemplate.opsForValue();
String recordData = ops.get("RecordData");
if(recordData!=null){
System.out.println("从数据库查询数据之前,有缓存数据。。。。");
return JSON.parseObject(recordData, new TypeReference<RecordEntity>() {});
}
System.out.println("从数据库查询数据....");
RecordEntity recordEntity = baseMapper.selectById(id);
ops.set("RecordData",JSON.toJSONString(recordEntity), 1, TimeUnit.DAYS);
return recordEntity;
}
}
//redis分布式锁
public RecordEntity getEntityByIdByFbs(Long id){
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = redisTemplate.opsForValue();
//保证原子性,加锁同时设置过期时间
Boolean lock = ops.setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功。。。。。");
RecordEntity recordEntity = null;
try{
recordEntity = getEntityById(id);
}finally {
//删除锁保证原子性,使用脚本
String script ="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return recordEntity;
}else{
System.out.println("获取分布式锁失败,等待重试。。。。");
//重试,可以等待sleep一下
try{
Thread.sleep(200);
}catch (Exception e){
}
return getEntityByIdByFbs(id);
}
}
//业务代码
public RecordEntity getEntityById(Long id){
ValueOperations<String, String> ops = redisTemplate.opsForValue();
String recordData = ops.get("RecordData");
if(recordData!=null){
System.out.println("从数据库查询数据之前,有缓存数据。。。。");
return JSON.parseObject(recordData, new TypeReference<RecordEntity>() {});
}
System.out.println("从数据库查询数据....");
RecordEntity recordEntity = baseMapper.selectById(id);
ops.set("RecordData",JSON.toJSONString(recordEntity), 1, TimeUnit.DAYS);
return recordEntity;
}
//Redisson分布式锁
public RecordEntity getEntityByIdByFbsRedisson(Long id){
//保证原子性,加锁同时设置过期时间
RLock lock = redissonClient.getLock("RecordData-lock");
lock.lock();
RecordEntity recordEntity = null;
try {
System.out.println("获取分布式锁成功。。。。。");
recordEntity = getEntityById(id);
}finally {
System.out.println("释放锁成功。。。。。");
lock.unlock();
}
return recordEntity;
}
// 使用缓存注解方式
@Override
@Cacheable(value = {"record"},key = "#root.method.name")
public RecordEntity getRecordAllInfoById(Long id) {
//未使用注解缓存方案
// RecordEntity recordEntity = null;
//
// ValueOperations<String, String> forValue = redisTemplate.opsForValue();
// String recordData = forValue.get("RecordData");
// if(recordData==null){
// System.out.println("缓存没数据,执行查询数据库方法。。。。");
// recordEntity = getEntityByIdByFbsRedisson(id);
// }else{
// System.out.println("从缓存中获取数据。。。。。");
// recordEntity = JSON.parseObject(recordData, new TypeReference<RecordEntity>() {
// });
// }
//使用注解缓存后
RecordEntity recordEntity = getEntityById(id);
if(recordEntity!=null){
Long categoryId = recordEntity.getCategoryId();
Long[] catelogPath = findCatelogPath(categoryId);
recordEntity.setCatelogPath(catelogPath);
}
return recordEntity;
}
利用redis的set NX实现分布式

redissonDistributed Lock,Implementation Principle就是利用redis的set nxImplementation的。
SET key value [EX seconds] [PX milliseconds] [NX|XX]

Distributed LockPrinciple可以看redis官方文档set nxCommand。
更多Distributed Lock可参考,另一个篇springboot整合Distributed Lockredisson。