目录
缓存更新策略
redis做缓存数据要和mysql数据库保持一致。
都是修改mysql数据库时把缓存清空,下一次查询时再构建缓存。
如果是先删除再修改数据库的话在数据库修改的过程中可能被其他线程查询然后构建缓存从而导致数据的不一致,如下图;
可以看到线程二在线程一删除后来查询了重构了缓存然后线程一修改了mysql数据库导致数据不一致。
我们选择先修改mysql数据库,再删除redis缓存,下一次查询时再构建缓存。这样就保证了数据的一致性。
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
解决办法-缓存空对象
比如查询用户【id=1】的用户。但是为【id=1】的用户数据库中不存在,我们也把【id=1】缓存到redis缓存中,并且设置过期时间。这样就可以防止每次查询【id=1】都会击穿redis缓存到mysql中查询。
优点:实现简单,维护方便
缺点:额外的内存消耗可能造成短期的不一致
如上例子防止缓存穿透。把【id=1】缓存到redis缓存中,如果这时候新增了一个用户【id=1】,但是缓存中记录了【id=1】是空对象这就导致了数据的不一致性。
可以看到合理设置过期时间是非常重要。
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案-互斥锁
这里实现思路类似双重检测的单利模式。保证了缓存构建之后被构建一次,有线程在构建时其他线程选择等待。
Redis实现秒杀
假设商品库存是10;
我们可以设置一个key【count=10】然后没抢一次就 【incrby count -1】
在调用【incrby count -1】会返回一个自减的值,redis是单线程也就保证了自减的原子性。在项目中判断一下库存是否为零就好。
代码实现
// 2.减少库存
long value = redisTemplate.opsForValue().increment(String.valueOf(key), -(long) num);
// 库存充足 可以异步操作,即使反馈购买成功的信息,并异步处理 扣减mysql数据库并生成订单
if (value >= 0) {
// update 数据库中商品库存和订单系统下单,单的状态未待支付
// 分开两个系统处理时,可以用LCN做分布式事务,但是也是有概率会订单系统的网络超时
// 也可以使用最终一致性的方式,更新库存成功后,发送mq,等待订单创建生成回调。
boolean res = productDao.update(num, key) == 1;
if (res) {
//创建订单
//createOrder(req);
//记录购买日志
productLogDao.insert(new CodeantProductLog( new Date(), key, num ));
}
return "成功购买";
} else {
//恢复扣减的redis库存
redisTemplate.opsForValue().increment(String.valueOf(key), (long) num);
return "redis库存不够";
}
Redis实现分布式锁
例如秒杀一个商品要求一个用户只能抢一次【一人一单】在单机时可以对用户id进行锁定
synchronized(userId){…}。但是如果是分布式情况下synchronized就不行了,因为synchronized只对同一个jvm有效。
redis分布式锁原理
命令【SETNX】添加一个String类型的键值对,前提是这个key不存在,否则不执行。
【SETNX】很显然就有锁的机制。
加锁:【SETNX lock lockvalue】
解锁:【del lock】
利用【SETNX】实现锁的话,锁是不可重入的,而且要设定有效时间,避免锁永远不释放。
Redisson解决
编码中自己用redis的【SETNX】去实现锁的话较为繁琐,因为要考虑到锁的可重入,以及锁过期时间的控制。
Redisson就是使用redis实现分布式锁的一套API
redis持久化
RDB持久化
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。fork采用的是copy-on-write技术:当主进程执行读操作时,访问共享内存;当主进程执行写操作时,则会拷贝一份数据,执行写操作。
RDB会在什么时候执行?save 60 1000代表什么含义?
默认是服务停止时。
【save 60 1000】Redis内部有触发RDB的机制,可以在redis.conf文件配置。代表60秒内至少执行1000次修改则触发RDB
RDB的缺点
RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
fork子进程、压缩、写出RDB文件都比较耗时
RDB持久化可以手动输入命令【bgsave】触发。
AOF持久化(默认是关闭的)
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
# 表示每执行一次写命令,立即记录到AOF文件
#appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
#appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
总结
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
Redis到底是单线程还是多线程?
Redis的命令处理是单线程。
Redis v6.0:在核心网络模型中引入 多线程,对于命令的解析以IO的传输是多线程。
redis过期策略
redis可以通过【expire】命令给Redis的key设置TTL(存活时间)
Redis是如何知道一个key是否过期呢?
redis底层数据结构中记录了过期时间
是不是TTL到期就立即删除了呢?
惰性删除:顾明思议并不是在TTL到期后就立刻删除,而是在访问一个key的时候,检查该key的存活时间,如果已经过期才执行删除。
周期删除:顾明思议是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。
redis淘汰策略
就是当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。
Redis支持8种不同策略来选择要删除的key:
noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
allkeys-random:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选
volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。
allkeys-lru: 对全体key,基于LRU算法进行淘汰
volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
allkeys-lfu: 对全体key,基于LFU算法进行淘汰
volatile-lfu: 对设置了TTL的key,基于LFI算法进行淘汰
LRU(Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
修改/etc/redis.config就行修改策略