Redis问题-redis常见问题解决
一、项目中为什么要用Redis
传统关系型数据库已经不适用于所有的场景,例如秒杀活动,很容易把数据库打崩,所以引入了缓存中间件,目前市面上比较常用的缓存中渐渐有Redis和Memcached。
二、Redis和Memcached的区别Redis:
速度快,因为数据在内存中,类似于HashMap。
支持丰富的数据类型
支持事务,操作都是原子操作
丰富的特性:可用于缓存、消息、设置过期时间、过期后自动删除等
Redis支持Cluster模式
Redis使用单核,所以平均到每个核上,Redis存储小数据会比Mem性能高;但100K以上的数据,Mem的性能高于Redis
三、Redis的线程模型Redis内部使用文件事件处理器,它是单线程的,所以Redis才叫单线程模型。它采用IO多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的EventHandler来处理。
多个Socket可能会发生并发操作,每个操作对应不同的文件事件,IO多路复用会监听多个Socket,会将Socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
到达过期的时间点时,Redis可能出现卡顿,严重时会出现雪崩。因为大量缓存同时失效,会造成大量请求到达数据库,造成数据库压力增加,崩溃。这时可以在过期时间后面加上一个随机值,使过期的分散一些。
五、分布式锁通过setnx抢锁,然后通过expire给锁加一个过期时间。
实现方法:
jedis.set(String key, String value, String nxxx, String expx, int time);第一个是key,我们使用 key来当锁,因为 key 是唯一的。
第二个是value,入参是 requestId,很多人不明白,有 key 作为锁不就够了吗,为何还要用到value?
因为我们上面讲到的可靠性里,分布式锁要满足第4个条件:解铃还须系铃人,通过给 value 赋值为 requestId,我们就知道这把锁是哪个请求加的,在解锁的时候可以有依据。
requestId 可以使用UUID.random().toString() 方法生成。
第三个是nxxx,这个参数 我们填的是NX,意思 set if not exist,即当 key 不存在时,我们进行 set 操作;若 key 已经存在,则不做任何操作;
第四个是expx,这个参数我们传的是PX,意思是我们要给这个 key 加一个过期的设置,具体时间由第五个参数决定;
第五个是time,于第四个参数相呼应,代表 key 的过期时间。
解锁的正确姿势:
public class RedisTool{
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @paramjedis Redis客户端
* @paramlockKey 锁
* @paramrequestId 请求标识
* @return是否释放成功
*/ public staticboolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call(get, KEYS[1]) == ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if(RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}script是一个lua脚本,通过jedis.eval方法,传入解锁的key,和对应的requestId。
这几行代码的作用是:先获取锁对应的value值,然后检查是否与reqeustId相等,相等则解锁。
为什么使用Lua脚本呢?因为要确保操作是原子性的,如果通过jedis.del(lockKey)解锁的话,因为没有判断拥有者就直接解锁,所以不安全;如果先判断requestId再决定要不要解锁时,因为是两个操作,不是原子操作,所以会有并发问题。使用eval方法传递给Redis服务端,它会当成一个命令执行,只有执行结束后,才会继续执行其他命令。
六、每秒上千订单场景下,如何对分布式锁的并发进行优化下单时为了防止库存超卖,每秒上千订单的高并发场景,如何对分布式锁进行高并发优化来应对这个场景?
首先想到库存超卖问题有很多解决方案,悲观锁、分布式锁、乐观锁、队列串行化,Redis原子操作。
针对分布式锁的解决方案,首先看库存超卖是怎么产生的:
使用分布式锁解决库存超卖问题:
区别在于两个订单系统,第一个订单系统进行交易时,会把库存锁住,第二个订单系统想要获取库存必须先等待第一个订单系统释放分布式锁,等第一个释放了之后,订单系统二获取到的库存是不足的,所以就无法下单了。
但是有一定的问题,加了分布式锁之后,其他服务的下单请求都必须等待,这样就成了串行处理。那么如何对分布式锁进行高并发优化?
分段加锁
可以把数据分成很多段,每个段是一个单独的锁,多个线程过来并发修改数据时,可以并发修改不同段的数据。
需要注意:如果下单过程中,加了锁,发现这个分段库存不足,应该立刻释放锁,然后换下一个分段库存,再加锁尝试处理。
七、使用Redis做异步队列使用List结构做队列,rpush生产消息,lpop消费消息。当lpop没有消息时,适当sleep等待生产者放入消息。或者通过blpop指令,如果没有消息就阻塞。当想要生产一次,消费多次时,可以通过pub/sub主题订阅方式,实现1:N的消息队列。
Redis如何实现延时队列:使用zset有序队列,使用时间戳作为socre,消息内容作为key,调用zadd生产消息,消费者用zrangebyscore指令获取N秒钱的数据进行处理。
八、Redis持久化Redis有两种持久化方式:RDB和AOF
RDB会保存当前数据库快照,全量持久化
AOF倾向于日志保存,做增量持久化。
停机重启时,通过RDB持久化文件重新构建内存,使用AOF重放近期的操作指令来完全恢复到停机前的状态。
如果机器突然停电,AOF日志的sync属性,如果不要求性能,每条写指令都会同步至磁盘;高性能条件下一般1s1同步,这样最多丢失1s的数据。
九、Redis同步机制Redis可以主从同步,从从同步。
第一次同步时,主节点做一个bgsave,同时将后续修改操作记录到内存buffer,完成后将RDB文件全量同步到负直接点,复制节点接收完成后将RDB
加载到内存。加载完成后,再通过主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步。
十、多个系统同时操作Redis带来的数据问题例如ABC三个系统,分别取Redis的同一个key,本来顺序1,2,3是正常的,但是因为A系统网络卡了一下,BC在他前面操作了Redis这样数据就有问题了。
可以通过上文提到的redis分布式锁,或者通过zookeeper实现分布式锁。
每个系统通过zookeeper获取分布式锁,确保同一时间,只能有一个系统在操作某个key,别人不允许读和写。
数据库和缓存的双写问题,一致性问题
一般都会允许缓存和数据库存在偶尔不一致的问题,如果必须要求一致性,则读请求和写请求串行化,串到一个内存队列中去。但是这样会造成吞吐量大幅度降低。
KV+DB读写模式:
读的时候先查缓存,缓存查不到再去查数据库,读取数据库的值放入缓存,返回响应。
更新的时候,先更新数据库,再删除缓存。




