springboot使用redis的两种方式与redis数据淘汰策略
redis的使用真的就是使用模板存取吗? 幼稚,那要增减呢! 所以今天聊聊redis的springbooot的两种使用方法。
1、springboot使用redis的方式
1.1、采用redisTemplate
它不是简单的引入模板就可以的,需要封装。因为模板仅仅可以操作字符串,配置redistemplate的序列化方式之后就可以顺利的执行increment、decrement。
①导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
②配置(需要配置密码的自己配置)
spring:
redis:
host: localhost
port: 6379
③封装类
@Component
@Slf4j
public class RedisService {
@Autowired
protected RedisTemplate redisTemplate;
/**
* 配置redistemplate的序列化方式之后就可以顺利的执行increment、decrement
* @param redisTemplate
*/
@Autowired(required = false)
public void setRedisTemplate(RedisTemplate redisTemplate) {
//序列化为String
RedisSerializer stringSerializer = new StringRedisSerializer();
//序列化为Json
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
this.redisTemplate = redisTemplate;
}
/**
* 对字符串的操作,存入key
* @param key
* @param value
* @return
*/
public boolean setString(String key,String value){
boolean state = true;
try{
redisTemplate.opsForValue().set(key,value);
}catch (Exception e){
state = false;
log.info("redis string type,set error,key:"+key+",value:"+value+",msg:"+e.getMessage());
}
return state;
}
/**
* 对字符串的操作,获取key
* @param key
* @return
*/
public String getString(String key){
try{
Object data = redisTemplate.opsForValue().get(key);
return String.valueOf(data);
}catch (Exception e){
log.info("redis string type,get error,key:"+key+",msg:"+e.getMessage());
}
return null;
}
/**
* 给key指定过期时间
* @param key
* @param expire
* @return
*/
public boolean expire(final String key, long expire) {
boolean state = true;
try{
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}catch (Exception e){
state = false;
log.info("redis string type,set error,key:"+key+",value:"+expire+",msg:"+e.getMessage());
}
return state;
}
/**
* 给key赋值为Map类型的value
* @param key
* @param value
* @return
*/
public boolean setMap(String key, Map value) {
boolean state = true;
try {
redisTemplate.opsForHash().putAll(key, value);
} catch (Exception e) {
state = false;
log.info("redis string type,set error,key:" + key + ",value:" + value + ",msg:" + e.getMessage());
}
return state;
}
/**
* 获取key对应的值是一个Map
* @param key
* @return
*/
public Map getMap(String key) {
try {
return redisTemplate.opsForHash().entries(key);
} catch (Exception e) {
log.info("redis string type,get error,key:" + key + ",msg:" + e.getMessage());
}
return null;
}
/**
* 给指定的key赋值value,并指定过期时间
* @author liaochao
* @Date 2020.04.09
* @param key
* @return
*/
public <T> boolean setCacheValueForTimes(String key, T value,long time,TimeUnit tu){
boolean state = true;
try{
redisTemplate.opsForValue().set(key, value, time, tu);
}catch (Exception e){
e.printStackTrace();
state = false;
log.info("redis string type,set error,key:"+key+",value:"+value+",msg:"+e.getMessage());
}
return state;
}
/**
* 对指定的key进行自增1
* @author liaochao
* @Date 2020.04.09
* @param key
* @return
*/
public long testInckey(String key){
long result = 0;
try{
result = redisTemplate.boundValueOps(key).increment(1);
}catch (Exception e){
e.printStackTrace();
log.info("redis string type,set error,key:temporary,value:"+result+",msg:"+e.getMessage());
result = -1;
}
return result;
}
/**
* 对指定的key进行自减1
* @author liaochao
* @Date 2020.04.09
* @param key
* @return
*/
public long testDeckey(String key){
long result = 0;
try{
result = redisTemplate.boundValueOps(key).decrement(1);
}catch (Exception e){
e.printStackTrace();
log.info("redis string type,set error,key:temporary,value:"+result+",msg:"+e.getMessage());
result = -1;
}
return result;
}
/**
* 获取key对应的value值,传入value的类型,获取出来也是转换了类型的value值
* @author liaochao
* @Date 2020.04.09
* @param key
* @return
*/
public <T> T getValue(String key,Class<T> clazz){
try{
String s = ""+ redisTemplate.opsForValue().get(key);
T value = JSON.parseObject(s,clazz);
return value;
}catch (Exception e){
e.printStackTrace();
log.info("redis string type,get error,key:"+key+",msg:"+e.getMessage());
}
return null;
}
/**
* 删除key
* @param key
* @return
*/
public boolean delete(String key){
boolean result = false;
try{
result = redisTemplate.delete(key);
}catch (Exception e){
log.info("redis string type,set error,key:"+key+",msg:"+e.getMessage());
}
return result;
}
}
这样在其他地方使用,引入这个类就可以了。这里没有写操作集合的,读者自己完成
。
1.2 、采用Jedis
①导包:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
②配置yml
spring:
redis:
host: localhost
password: redis
port: 6379
# Redis数据库索引(默认为0)
database: 0
# 连接超时时间(毫秒)
timeout: 0
jedis:
pool:
# 最大连接池数
max-active: 100
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 20
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 100
③写一个JedisPool配置类
@Configuration // 配置类
@EnableCaching // 开启缓存(可不写这个注解)
public class RedisConfig {
@Value("${spring.redis.host}") // 注入配置文件中的属性
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
@Bean
public JedisPool getJedisPool() {
log.info("JedisPool注入成功!!");
log.info("redis地址:" + host + ":" + port);
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis * 1000);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
return jedisPool;
}
}
④封装redis的操作方法的类
@Service
public class RedisService {
@Autowire
private JedisPool jedisPool;
private Jedis getResource() {
return jedisPool.getResource();
}
private void returnResource(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
// 获取
public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {
Jedis jedis = null;
try {
jedis = getResource();
String str = jedis.get(prefix.getPrefix() + key);
return JSON.parseObject(str, clazz);
} finally {
returnResource(jedis);
}
}
// 存储
public <T> boolean set(KeyPrefix prefix, String key, T value) {
if (value == null || key == null) {
return false;
}
Jedis jedis = null;
try {
jedis = getResource();
if (prefix.expireSeconds() <= 0) {
jedis.set(prefix.getPrefix() + key, JSON.toJSONString(value));
} else {
// 设置有效期
jedis.setex(prefix.getPrefix() + key, prefix.expireSeconds(), JSON.toJSONString(value));
}
log.info("存入redis,key={}", (prefix.getPrefix() + key));
return true;
} finally {
returnResource(jedis);
}
}
// 判断key是否存在
public boolean exists(KeyPrefix prefix, String key) {
if (key == null) {
return false;
}
Jedis jedis = null;
try {
jedis = getResource();
return jedis.exists(prefix.getPrefix() + key);
} finally {
returnResource(jedis);
}
}
// 数字值增加一
public Long incr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.incr(prefix.getPrefix() + key);
} finally {
returnResource(jedis);
}
}
// 数字值减少一
public Long decr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.decr(prefix.getPrefix() + key);
} finally {
returnResource(jedis);
}
}
// 删除
public boolean delete(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = getResource();
return jedis.del(prefix.getPrefix() + key) > 0;
} finally {
returnResource(jedis);
}
}
这样在其他地方使用时,引入这个RedisService 就可以了。
题外话:今天遇到double类型保留两位小数的方法
例如:a是一个Double类型,有很多位小数,要把它保留两位。 BigDecimal b = new BigDecimal(a);
Double s = b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
这样得到的s就是a保留两位小数的结果了!
spring boot 2.0后集成的redis,在高并发的情况下报 异常(对外内存溢出异常): Redis exception; nested exception is io.lettuce.core.RedisException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate37748736 byte(s) of direct
原因有如下:
1、spring boot 2.0以后默认使用的是lettuce作为操作redis的客户端。它使用netty进行网络通信;
2、lettuce的bug导致netty堆外内存溢出,netty如果没有指定堆外内存,默认使用 -Xmx100m(堆的大小);
netty可以通过
-Dio.netty.maxDirectMemory进行设置;
解决方案
:不要使用-Dio.netty.maxDirectMemory进行调大堆外内存;
a、升级lettuce 客户端;
b、切换到jedis;
例如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
这样引包就可以排除lettuce的bug,而使用jedis作底层连接。那就可以放心使用springboot2.0
去集成redisTemplate不会出现高并发下的对外内存溢出
2、 缓存失效问题
缓存穿透
是指查询一个一定不存在的数据,由于缓存是不命中,将>去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null写入缓存,这将导致这个不存在的数据每次 请求都要到存储层去查询,失去了缓存的意义
解决办法
:缓存空结果、并且设置短的过期时间
缓存击穿
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所 有对这个 key 的数据查询都落到
db,我们称为缓存击穿
解决办法
:加锁(单体部署就加本地锁,分布式的话就要用分布式锁)
缓存雪崩
是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失 效,请求全部转发到 DB,DB 瞬时压力过重雪崩
解决办法
:原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件
3、redis数据淘汰策略
一共8种
- volatile-lru,针对设置了过期时间的key,使用lru算法进行淘汰。
- allkeys-lru,针对所有key使用lru算法进行淘汰。
- volatile-lfu,针对设置了过期时间的key,使用lfu算法进行淘汰。
- allkeys-lfu,针对所有key使用lfu算法进行淘汰。
- volatile-random,从所有设置了过期时间的key中使用随机淘汰的方式进行淘汰。
- allkeys-random,针对所有的key使用随机淘汰机制进行淘汰。
- volatile-ttl,删除生存时间最近的一个键。
- noeviction(默认),不删除键,值返回错误。 主要是4种算法,针对不同的key,形成的策略。 算法:
4种算法
lru
LRU是Least Recently Used的缩写,也就是表示最近很少使用,也可以理解成最久没有使用
lfu
LFU(Least Frequently Used),表示最近最少使用,它和key的使用次数有关,其思想是:根据key最近被访问的频率进行淘汰,比较少访问的key优先淘汰,反之则保留。
random
随机淘汰 ttl 快要过期的先淘汰 key :
volatile
有过期的时间的那些key allkeys 所有的key
工作原理
客户端执行一条新命令,导致数据库需要增加数据(比如set key value) Redis会检查内存使用,如果内存使用超过
maxmemory,就会按照置换策略删除一些 key 新的命令执行成功