redis
redis是现在最受欢迎的NoSQL数据库之一,包含多种数据结构,其具有以下特性:
- 基于内存运行,性能高效
- 支持分布式,理论上可以无限扩展
- key-value存储系统
- 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
为什么使用redis
主要基于性能和并发两方面考虑
性能:对于一些“热点”数据(高频读,低频写),或者是一些执行耗时特别久,且结果频繁不动的数据,这样的数据特别适合放入缓存中处理。这样后面的请求就可以从缓存中去读取数据,使得请求能迅速响应。
并发:在上图中可以看到,一个对数据库的请求就会消耗大量的时间,如果在大并发的情况下,所有请求直接访问数据库,数据库可能会出现连接异常。这时候就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。
redis主要数据类型及其特性
redis提供的数据类型主要分为5种自有类型和一种自定义类型,这5种自有类型包括:String类型、哈希类型、列表类型、集合类型和顺序集合类型。
String类型
它是一个二进制安全的字符串,意味着它不仅能够存储字符串、还能存储图片、视频等多种类型,最大长度支持512M。
对每种数据类型,redis都提供了丰富的操作命令,如:
- GET key / MGET key1[key2…]
获取指定key的值 / 获取所有(一个或多个)指定key的值 - SET key value / SETEX key seconds value
设置指定key的值 / 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位) - MSET key value [key value…] / MSETNX key value [key value…]
同时设置一个或多个 key-value 对 / 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 - GETSET key value
将给定 key 的值设为 value ,并返回 key 的旧值,没有旧值返回(nil)
Hash(哈希)
它是一个键值对集合,是 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
常用操作命令如下:
-
HGET key field / HMGET key field1 [field2] / HGETALL key
获取存储在哈希表中指定字段的值 / 获取所有给定字段的值 / 获取所有哈希表中的字段 -
HSET key field value / HMSET key field1 value1 [field2 value2 ] / HSETNX key field value
将哈希表 key 中的字段 field 的值设为 value / 同时将多个 field-value 对设置到哈希表 key 中 / 只有在字段 field 不存在时,设置哈希表字段的值 -
HEXISTS key field / HLEN key
查看哈希表 key 中,指定的字段是否存在 / 获取哈希表中字段的数量 -
HKEYS key / HDEL key field1[field2…]
获取所有哈希表中的字段 / 删除一个或多个哈希表字段 -
HVALS key
获取哈希表中所有值
List(列表)
简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
常用操作命令如下:
- LPUSH key value1[value2] / LPUSHX key value
将一个或多个值插入到列表头部(在Redis 2.4版本以前的 LPUSH 命令,都只接受单个 value 值)/ 将一个值插入到已存在的列表头部 - RPUSH key value1[value2] / RPUSHX key value
将一个或多个值插入到列表的尾部(在Redis 2.4版本以前的 RPUSH 命令,都只接受单个 value 值) / 将一个值插入到已存在的列表尾部 - LINSERT key BEFORE|AFTER pivot value / LSET key index value
将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。/ 通过索引来设置元素的值 - LRANGE key start stop
返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 - LPOP key / RPOP key
移出并获取列表的第一个元素 / 移出并获取列表的最后一个元素
Set(集合)
Set类型是一种无顺序集合, 它和List类型最大的区别是:集合中的元素没有顺序, 且元素是唯一的。
Set类型的底层是通过哈希表实现的,常用操作命令如下:
- SADD key member1 [member2] / SCARD key
向集合添加一个或多个成员(在Redis 2.4版本以前的 SADD命令,只接受单个成员值)/ 获取集合的成员数 - SPOP key / SREM key member1 [member2]
移除并返回集合中的一个随机元素 / 移除集合中一个或多个成员(在Redis 2.4版本以前的 SREM命令,只接受单个成员值) - SMOVE source destination member
将 member 元素从 source 集合移动到 destination 集合 - SINTER key1 [key2] / SUNION key1 [key2] / SDIFF key1 [key2]
返回给定所有集合的交集 / 返回给定所有集合的并集 / 返回第一个集合与其他集合之间的差异
ZSet(有序集合)
ZSet是一种有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从小到大的排序。。
常用操作命令如下:
- ZADD key score1 member1 [score2 member2] / ZREM key member [member …]
向有序集合添加一个或多个成员,或者更新已存在成员的分数(在 Redis 2.4 版本以前, ZADD 每次只能添加一个元素)/ 移除有序集合中的一个或多个成员(在 Redis 2.4 版本以前, ZREM 每次只能删除一个元素) - ZCOUNT key min max / ZCARD key
计算在有序集合中指定区间分数的成员数 / 获取有序集合的成员数 - ZRANGE key start stop [WITHSCORES] / ZRANGEBYLEX key min max [LIMIT offset count] / ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]
通过索引区间返回有序集合指定区间内的成员 / 通过字典区间返回有序集合的成员 / 通过分数返回有序集合指定区间内的成员 - ZINTERSTORE destination numkeys key [key …] / ZUNIONSTORE destination numkeys key [key …]
计算给定的一个或多个有序集(numkeys)的交集并将结果集存储在新的有序集合 destination 中 / 计算给定的一个或多个有序集(numkeys)的并集集并将结果集存储在新的有序集合 destination 中
基于gradle搭建的springboot工程集成使用redis
1. 在build.gradle中添加redis依赖
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.apache.commons:commons-pool2'
2. 在application.yml文件中配置redis基本配置
spring:
redis:
database: 0 # Redis数据库索引(默认为0)
host: localhost # Redis服务器地址
port: 6379 # Redis服务器连接端口
password: # Redis服务器连接密码(默认为空)
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-idle: 8 # 连接池中的最大空闲连接 默认 8
min-idle: 0 # 连接池中的最小空闲连接 默认 0
3. 添加配置类 RedisConfig.java
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(factory).cacheDefaults(config).transactionAware().build();
}
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 创建Redis缓存操作助手RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 以下代码为将RedisTemplate的Value序列化方式由JdkSerializationRedisSerializer更换为Jackson2JsonRedisSerializer
// 此种序列化方式结果清晰、容易阅读、存储字节少、速度快,所以推荐更换
Jackson2JsonRedisSerializer<Object> 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);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4. 添加RedisUtil.java工具类
@Component
public class RedisUtil{
@Autowired
private RedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
if (null == redisTemplate) {
log.info("Redis初始化配置失败,请检查配置项");
} else {
log.info("Redis初始化配置注入成功!");
}
this.redisTemplate = redisTemplate;
//=============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key,long time){
try {
if(time>0){
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String ... key){
if(key!=null&&key.length>0){
if(key.length==1){
redisTemplate.delete(key[0]);
}else{
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key, int indexdb){
redisTemplate.indexdb.set(indexdb);
return key==null?null:redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key,Object value,int indexdb) {
try {
redisTemplate.indexdb.set(indexdb);
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key,Object value,long time){
try {
if(time>0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}else{
redisTemplate.opsForValue().set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param by 要增加几(大于0)
* @return
*/
public long incr(String key, long delta){
if(delta<0){
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param by 要减少几(小于0)
* @return
*/
public long decr(String key, long delta){
if(delta<0){
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key,String item){
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object,Object> hmget(String key){
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String,Object> map){
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String,Object> map, long time){
try {
redisTemplate.opsForHash().putAll(key, map);
if(time>0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key,String item,Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key,String item,Object value,long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if(time>0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item){
redisTemplate.opsForHash().delete(key,item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item){
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item,double by){
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item,double by){
return redisTemplate.opsForHash().increment(key, item,-by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key){
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key,Object value){
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object...values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key,long time,Object...values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if(time>0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key){
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object ...values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key,long start, long end){
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key){
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key,long index){
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index,Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key,long count,Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
redis缓存过期机制
-
缓存过期
定期删除:指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除
惰性删除:在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了,如果过期了此时就会删除,不会给你返回任何东西 -
缓存淘汰
redis是基于内存的key-value数据库,内存是有限的宝贵资源,当内存耗尽的时候,redis有如下6种处理方式来应对
1.No-eviction
在该策略下,如果继续向redis中添加数据,那么redis会直接返回错误
2.Allkeys-lru
从所有的key中使用LRU算法进行淘汰
3.Volatile-lru
从设置了过期时间的key中使用LRU算法进行淘汰
4.Allkeys-random
从所有key中随机淘汰数据
5.Volatile-random
从设置了过期时间的key中随机淘汰
6.Volatile-ttl
从设置了过期时间的key中,选择剩余存活时间最短的key进行淘汰
使用缓存常见问题
1. 缓存和数据库双写一致性问题
读取缓存方面,按照以下过程进行
更新策略:
(1)先更新数据库,再更新缓存,不可行
- 原因一(线程安全方面)
同时有请求A和请求B进行更新操作,会出现:
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。 - 原因二(业务场景方面)
(1)如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
(2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。
(2)先删除缓存,再更新数据库
该方案会导致不一致的原因是,同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
那么,如何解决呢?采用延时双删策略,伪代码如下
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
针对上面的情形,开发者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
如果使用mysql读写分离架构
在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值
上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
采用这种同步淘汰策略,吞吐量降低怎么办?
ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
第二次删除,如果删除失败怎么办?
这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
(6)请求A试图去删除请求B写入的缓存值,结果失败了。
ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。
这种情况下,如果更新数据库成功,删缓存失败了,就会出现不一致的情况,利用重试机制解决这个问题。
该方案有一个缺点,对业务线代码造成大量的侵入,于是有了方案二:
当然,如果要解决数据库和缓存双写一致性的问题,给缓存设置过期时间是最好的方式
2. 缓存雪崩
现象: 大量key同一时间点失效,同时又有大量请求打进来,导致流量直接打在DB上,造成DB不可用。
解决方案:
- 设置key永不失效(热点数据)
- 设置key缓存失效时候尽可能错开
- 使用多级缓存机制,比如同时使用redsi和memcache缓存,请求->redis->memcache->db
- 购买第三方可靠性高的Redis云服务器
3.缓存击穿
现象: 用户大量并发请求的数据(key)对应的数据在redis和数据库中都不存在,导致尽管数据不存在但还是每次都会进行查DB。
解决方案:
- 从DB中查询出来数据为空,也进行空数据的缓存,避免DB数据为空也每次都进行数据库查询
- 使用布隆过滤器,但是会增加一定的复杂度及存在一定的误判率
bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。
可用google guava cache实现布隆过滤器,它需要在缓存之前再加一道屏障,里面存储目前数据库中存在的所有key,如下图所示
当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在。若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。若存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。
public class BloomFilterTest {
private static final int capacity = 1000000;
private static final int key = 999998;
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
public String getByKey(String key) {
// 通过key获取value
String value = redisService.get(key);
if (StringUtil.isEmpty(value)) {
if (bloomFilter.mightContain(key)) {
value = userService.getById(key);
redisService.set(key, value);
return value;
} else {
return null;
}
}
return value;
}
4.缓存并发竞争
现象: 多客户端同时并发写一个key,一个key的值是1,本来按顺序修改为2,3,4,最后是4,但是顺序变成了4,3,2,最后变成了2。
如何解决redis的并发竞争key问题?
1.利用消息队列
在并发量过大的情况下,可以通过消息队列中间件进行处理,把并行读写进行串行化。把redis.set操作放在队列中,使其串行化,必须一个一个的执行。
2.分布式锁+时间戳
首先使用分布式锁,确保同一时间,只能有一个系统实例在操作某个key。然后修改key的值时,要先判断这值的时间戳是否比缓存里的值的时间戳更靠后,如果是旧数据就不要更新了