redis

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的值时,要先判断这值的时间戳是否比缓存里的值的时间戳更靠后,如果是旧数据就不要更新了
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值