操作系统为centos7.5
redis的安装
```txt
1. 拿取镜像
docker pull redis
2. 创建配置文件目录
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
3. 启动
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis.server /etc/redis/redis.conf
4. 设置自启动
docker update redis --restart=always
```
代码整合redis
- 导入依赖
<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> - 配置redis
spring redis host: username: password: - 使用StringRedisTemplate
数据库与缓存一致性
问题
- 几种缓存使用模式
- 删除缓存,还是更新缓存?
- 先操作数据库还是先操作缓存?
- 如何保证数据的最终一致性?
解决方案
-
几种缓存使用模式
- Cache-Aside Pattern:旁路缓存模式
- Read-Through/Write-Through(读写穿透)
- Write-behind(异步缓存写入)
旁路缓存的读数据流程
- 读的时候,先读缓存,缓存命中的话,直接返回数据 - 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应旁路缓存的写数据流程
写请求,更新数据库,删除旧缓存 -
使用删除缓存替换更新缓存可以防止脏数据的问题。
-
Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存。
-
如何保证数据一致性
- 缓存延时双删
- 删除缓存重试机制
- 读取biglog异步删除缓存
5. 使用分布式读写锁和设置数据过期时间(大体思路)。常规数据(读多写少,即时性,一致性要求不高的数据)完全可以使用spring-Cache springCache
缓存穿透
问题
缓存穿透:指查询一个一定不存在的数据,由于缓存不命中时,需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
解决方案
- 如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
- 如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有写请求进来的话,需要更新缓存,以保证一致性,同时,最后给缓存值设置适当的过期时间。
- 使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
缓存雪崩
问题
缓存雪崩:指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
解决方案
- 缓存雪崩一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如果采用一个较大固定值+一个较小随机值,5小时+0-1800秒这样。
- Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群
缓存击穿
问题
缓存击穿: 指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。 缓存击穿看着有点像缓存雪崩,其实它两区别是,缓存雪崩是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别,是在于击穿针对某一热点key缓存,雪奔则是很多key。
解决方案
- 使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
//多个请求同时查询缓存为空 //实现方法 1. synchronized(this)或者添加到逻辑代码之上;(springboot组件为单例) 1. 先看缓存中是否存在 2. 若不存在则查询数据库 3. 放入缓存 **本地锁**在分布是中存在问题 2. 分布式锁 1. 占分布锁(原子命令) Boolean lock redisTemplate.opsForValue().setIfAbsent("lock",uuid,30,TimeUnit.Second); 2. lock 为true表示加锁成功 执行业务 3. 加锁失败 重试 4. 解锁 查询锁的值与创建的值相同开始删锁,使用lua脚本 /*使用lua脚本来锁*/ // 定义lua 脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; // 使用redis执行lua执行 DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(script); // 设置一下返回值类型 为Long // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型, // 那么返回字符串与0 会有发生错误。 redisScript.setResultType(Long.class); // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。 redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid); - “永不过期” ,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
- springboot集成Redisson实现分布式锁操作
-
导入依赖 org.redisson redisson
-
配置
@Configuration @ComponentScan @EnableCaching public static class Application { @Bean(destroyMethod="shutdown") RedissonClient redisson() throws IOException { RedissonClient redisson = Redisson.create(); Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.56.10:6379"); RedissonClient redisson = Redisson.create(config); return redisson; } } -
锁操作
-
缓存热Key
问题
在Redis中,我们把访问频率高的key,称为热点key。如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
解决方案
-
Redis集群扩容:增加分片副本,均衡读流量;
-
对热key进行hash散列,比如将一个key备份为key1,key2……keyN,同样的数据N个备份,N个备份分布到不同分片,访问时可随机访问N个备份中的一个,进一步分担读流量;
-
使用二级缓存,即JVM本地缓存,减少Redis的读请求。
其他
不同的业务场景,Redis选择适合的数据结构
- 排行榜适合用zset
- 缓存用户信息一般用hash
- 消息队列,文章列表适用用list
- 用户标签、社交需求一般用set
- 计数器、分布式锁等一般用String类型
Redis一些有坑的命令
- 不能使用 keys指令
- 慎用O(n)复杂度命令,如hgetall等
- 慎用Redis的monitor命令
- 禁止使用flushall、flushdb
- 注意使用del命令