Redis为什么这么快?
主要有三方面原因:
存储方式
Redis的存储是基于内存
的,直接访问内存的速度是远远大于访问磁盘的速度的。
一般情况下,计算机访问一次SSD磁盘的时间大概是50~150微秒;如果是传统的硬盘,需要的时间更长,大概是1~10毫秒;而访问一次内存的时间大概是120纳秒。因此,可见访问的速度差了快一千倍左右。
优秀的线程模型和IO模型
Redis使用单个主线程来执行命令,不需要进行线程切换,避免了上下文切换带来的性能开销,大大提高了Redis的运行效率和响应速度。
Redis采用了I/O多路复用技术,实现了单个线程同时处理多个客户端连接的能力,从而提高Redis的并发能力。
不过,Redis并不是一直都是单线程的,从4.0开始,Redis引入了Unlink这类命令,用于异步执行删除等操作,还有在6.0之后,Redis为了进一步提升I/O的性能,引入了多线程机制,利用多线程机制并发处理网络请求,从而减少Redis由于网络I/O等待造成的影响。
高效的数据结构
Redis本身提供了丰富的数据结构,比如:String、Hash、Zset等,这些数据结构大多操作的时间复杂度都是O(1)。
简单来说,Redis集群就是通过多台机器分担单台机器上的压力。
当单机Redis缓存的数据量太大,请求量也高,这个时候,就可以采用Redis集群(Redis Cluster)的方案。
Redis集群会将数据分片存储到多台Redis上,多个Redis实例都可进行读写操作(每个分片内部还是有主从结构,目的是为了提高集群的可用性)。
集群内每个节点都会保存集群的完整拓扑信息,包括每个节点的ID、IP地址、端口、负责的哈希槽范围等,它们直接通过Gossip协议保持通信,会周期性地发送PING和PONG消息,交换集群信息,使得集群信息得以同步。
Redis集群分片原理
Redis集群会将数据分散到16384(2^14)个哈希槽中,集群中的每个节点负责一定范围的哈希槽。
每个节点会拥有一部分的槽位,然后对应的键值会根据其本身的Key,映射到一个哈希槽中:
Redis客户端可以访问集群中任意一台实例,正常情况下这个实例包含这个数据。
但如果槽被转移了,客户端还未来得及更新槽的信息,当前实例没有这个数据,则返回MOVED响应给客户端,将其重定向到对应的实例。
为什么哈希槽节点的数目是16384?
如果是基于Redis来实现分布式锁,则需要利用SET EX NX
命令 + lua脚本。
加锁:
SET lock_key unique_value EX expire_time NX
解锁(使用lua脚本):
if redis.call("GET",KEYS[1]) == ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
唯一标识
,为了防止被别的客户端给释放了。
假设没有这个唯一值:
此时客户端B就会一脸懵逼,我还在执行呢,锁怎么就被别人释放了??
所以每个 客户端/每个线程
加锁时,需要设置一个唯一标识,比如uuid,防止锁被别的客户端误释放。
因为需要先判断锁的值和唯一标识
是否一致,一致后再删除释放锁,这里就涉及到两步操作,所以需要使用lua脚本才能保证原子性,这也是为什么释放锁要使用lua脚本的原因。过期机制
,假设某个客户端加了锁之后宕机了,锁没有设置过期机制,会使得其他客户端都无法抢到锁。
EX expire_time
就是设置锁的过期,单位是秒;还有PX
也是过期时间,单位是毫秒。SETNX
,即SET if Not eXists,它表示如果key已存在,则什么都不会做,返回0,如果不存在则会设置它的值,返回1。
那个时候,SETNX和过期时间的设置就无法保证原子性,如果客户端在发送完SETNX之后就宕机了,还没来得及设置过期时间,一样会导致锁不会被释放。
因此在2.6.12版本之后,优化了SET命令,使得可以执行SET EX PX。扩展:如何进行幂等性设计?避免项目中出现多笔重复的交易订单
数据库唯一约束
可以将订单号作为数据库的主键或唯一索引,这样一来,数据库就会拒绝重复插入的情况,避免重复订单。
分布式锁
可以利用分布式锁来避免多个请求同时处理同一笔订单的情况,比如使用Redis来实现:
String lockKey = "order_lock_" + orderNo;
boolean isLocked = redisTemplate.opsForValue();
if (isLocked){
try {
// 处理订单逻辑
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 已有请求在处理订单
}
PS:这里设置lockKey时使用"order_lock_" + orderNo
这种以订单号的维度加锁,避免同笔订单多次插入的同时锁的粒度也足够细。
假设仅使用"order_lock"
作为lockKey,那么下单方法的并发度就是1,严重影响性能,会导致请求阻塞引发系统崩溃。