主要原因:内存碎片引起的
内存碎片的产生主要是:(1)分配机制 (2)键值对大小不一样和删改操作
看 INFO memory 命令中的 mem_fragmentation_ratio
Redis 当前的内存碎片率指标。 mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。而大于 1.5 后则认为不是合理的范畴。
启动碎片清理即可。
config set activedefrag yes
这个命令只是启用了自动清理功能,但是,具体什么时候清理,会受到下面这两个参数的控制。这两个参数分别设置了触发内存清理的一个条件,如果同时满足这两个条件,就开始清理。在清理的过程中,只要有一个条件不满足了,就停止自动清理。
一旦被淘汰的数据选定后,如果这个数据是没被修改过数据,那么我们就直接删除;如果这个数据被修改过,我们需要把它写回数据库。
首先回忆下只读缓存的处理流程:
针对上述情况还是会出现 两者操作上的不一致性,那么就还是需要保证同时更新缓存和数据库。
同步直写需要保证同时更新缓存和数据库
。所以,我们要在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性
,也就是说,两者要不一起更新,要不都不更新,返回错误信息,进行重试。否则,我们就无法实现同步直写。因为是 保证同时更新缓存和数据库。所以总的排列组合只有两种。
(1)先 操作 缓存 , 后操作 数据库
(2)先 操作 数据库 ,后操作 缓存
在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。
客户端对数据的修改操作步骤主要有:
把这个流程叫做“读取 - 修改 - 写回”操作(Read-Modify-Write,简称为 RMW 操作)。当有多个客户端对同一份数据执行 RMW 操作的话,我们就需要让 RMW 操作涉及的代码以原子性方式执行。访问同一份数据的 RMW 操作代码,就叫做临界区代码。
其实就是在 Redis 中保存一个 key:value 就行了。
加锁包含了三个操作(读取锁变量、判断锁变量值以及把锁变量值设置为 1
),而这三个操作在执行时需要保证原子性。那怎么保证原子性呢?
原子性有两种实现方式:
在这里可以直接使用 SETNX 和 DEL 命令组合来实现加锁和释放锁操作
那么这样做会存在什么问题呐?
针对这个问题,一个有效的解决方法是,给锁变量设置一个过期时间。这样一来,即使持有锁的客户端发生了异常,无法主动地释放锁,Redis 也会根据锁变量的过期时间,在锁变量过期后,把它删除。其它客户端在锁变量过期后,就可以重新请求加锁,这就不会出现无法加锁的问题了。
本质上来讲,其实就是要能区分来自不同客户端的锁操作。
SETNX 命令,对于不存在的键值对,它会先创建再设置值(也就是“不存在即设置”),为了能达到和 SETNX 命令一样的效果,Redis 给 SET 命令提供了类似的选项 NX,用来实现“不存在即设置”。如果使用了 NX 选项,SET 命令只有在键值对不存在时,才会进行设置,否则不做赋值操作。此外,SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间。
举个例子,执行下面的命令时,只有 key 不存在时,SET 才会创建 key,并对 key 进行赋值。另外,key 的存活时间由 seconds 或者 milliseconds 选项值来决定。
SET key value [EX seconds | PX milliseconds] [NX]
因此,加锁操作可以:
// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000
解锁操作可以:
// KEYS[1]表示 lock_key,ARGV[1]是当前客户端的唯一标识,
// 这两个值都是我们在执行 Lua 脚本时作为参数传入的。
//释放锁 比较 unique_value 是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
因为,释放锁操作的逻辑也包含了读取锁变量、判断值、删除锁变量的多个操作。所以需要使用 Lua 脚本(unlock.script)实现的释放锁操作的伪代码。
redis-cli --eval unlock.script lock_key , unique_value
为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。
分为三步:
所谓的脑裂,就是指在主从集群中,同时有两个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。
那么为什么脑裂会导致数据丢失呐?
主从切换后,从库一旦升级为新主库,哨兵就会让原主库执行 slave of 命令,和新主库重新进行全量同步。而在 全量同步 执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。
所以原主库会丢失切换期间保存的数据!
既然问题是出在原主库发生假故障后仍然能接收请求上,我们就开始在主从集群机制的配置项中查找是否有限制主库接收请求的设置。
通过查找,我们发现,Redis 已经提供了两个配置项来限制主库的请求处理,分别是 min-slaves-to-write 和 min-slaves-max-lag。
设置的建议:假设从库有 K 个,可以将 min-slaves-to-write 设置为 K/2+1(如果 K 等于 1,就设为 1),将 min-slaves-max-lag 设置为十几秒(例如 10~20s),在这个配置下,如果有一半以上的从库和主库进行的 ACK 消息延迟超过十几秒,我们就禁止主库接收客户端写请求。
订单处理可以在数据库中执行,但库存扣减操作,不能交给后端数据库处理。当库存查验完成后,一旦库存有余量,我们就立即在 Redis 中扣减库存。而且,为了避免请求查询到旧的库存值,库存查验和库存扣减这两个操作需要保证原子性。
数据倾斜有两类。
这个方法的具体做法是,我们把热点数据复制多份,在每一个数据副本的 key 中增加一个随机前缀,让它和其它副本数据不会被映射到同一个 Slot 中。这样一来,热点数据既有多个副本可以同时服务请求,同时,这些副本数据的 key 又不一样,会被映射到不同的 Slot 中。在给这些 Slot 分配实例时,我们也要注意把它们分配到不同的实例上,那么,热点数据的访问压力就被分散到不同的实例上了。