在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 高可用的获取锁与释放锁; 高性能的获取锁与释放锁; 具备可重入特性; 具备锁失效机制,防止死锁; 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁的特性是排他性、避免死锁、高可用。
- 排他性:不管任何时候,只有一个客户端能持有同一个锁,保证操作的锁是自己的。
- 避免死锁:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。
- 高可用容错:只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。
分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)、Redis的setnx()命令、Zookeeper,etcd等。
官方权威的用Redis实现分布式锁管理器的算法,我们把这个算法称为RedLock。
redis分布式锁常用一下命令:
设置锁
SET {分布式锁key} {分布式锁值} NX PX {过期时间}
注意:
- 必须设置有效期,防止死锁产生
- 加锁时,每个节点产生一个随机数,防止多节点锁被误删除
- 写入值和设置失效时间是同时的,保持原子性,
解锁
匹配随机值,业务结束,删除key对应的数据。
注意:
- 获取数据,判断一致性,删除,必须同时进行,保持原子性。
Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
编程语言常用eval函数/方法执行。
可重入式锁
也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。例如:一个方法获取到锁之后,可能在方法内调这个方法此时就获取不到锁了。
缓存雪崩、缓存击穿、缓存穿透
- 获取锁:首先尝试获取锁,如果获取失败,判断这个锁是否是自己的,如果是则允许再次获取, 而且必须记录重复获取锁的次数。
- 释放锁:释放锁不能直接删除了,因为锁是可重入的,如果锁进入了多次,在内层直接删除锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。
缓存穿透
缓存穿透是指不断的请求缓存中不存在的数据, 则所有请求全部落在了数据库层, 高并发的情况下, 就直接影响了整个系统的业务, 甚至可能导致系统崩溃
解决
- 增加校验,如用户鉴权校验,id规则校验。
- 设置空对象缓存,数据库取出来的数据如果是空值, 同样也缓存, 但是设置一个比较短的缓存时间, 这样可以一定意义上减缓缓存穿透。
- 布隆过滤器。布隆过滤器本质上一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在。
缓存雪崩
缓存雪崩是指缓存中数据大批量同一时间过期或者redis服务挂了,而查询数据量巨大,引起数据库压力过大。
解决
1.业务分级缓存,可以在做数据缓存的时候, 按分类进行缓存, 添加不同的缓存时间2.缓存的同时, 对缓存时间加上一个随机数, 以至于不会让所有缓存同一时间大量失效3.对于redis服务挂掉的问题,可以实现redis的高可用主从架构, 并且做redis的持久化, 在redis挂掉的同时时读取本地缓存数据, 同时恢复redis服务加载持久化的数据
缓存击穿
概念
缓存击穿和缓存雪崩有点像, 不过不是大面积的缓存失效,热点key。缓存击穿指的是缓存中某一个key的值不断的接收着大量的请求, 而在这个key值失效的瞬间, 大量的请求落在了数据库上, 从而导致数据库可能压力过大
解决
1、加分布式锁或者分布式队列
用加分布式锁或者分布式队列的方式保证缓存的单线程写,从而避免失效时大量的并发请求落到底层存储系统上。在加锁方法内先从缓存中再获取一次(防止另外的线程优先获取锁已经写入了缓存),没有再查DB写入缓存。 (当然也可以: 在没有获取锁的线程中一直轮询缓存,至超时)
2、添加超时标记
在缓存的对象上增加一个属性来标识超时时间,当获取到数据后,校验数据内部的标记时间,判定是否快超时了,如果是,异步发起一个线程(控制好并发)去主动更新该缓存。
3、另外还有一个粗暴的方法,如果你的热点数据要求实时性比较低,那么可以设置热点数据在热点时段不过期,在访问低峰期过期,比如每天凌晨过期。
4、冷热数据分离。
Redis是单线程的,但Redis为什么这么快?Redis内存划分
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路I/O复用模型,非阻塞IO;多指的是多个网络连接,复用指的是复用同一个线程
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
- 数据
作为数据库,数据是最主要的部分;这部分占用的内存会统计在used_memory中。
- 进程本身运行需要的内存
Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。
- 缓冲内存
缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;其中,客户端缓冲存储客户端连接的输入输出缓冲;复制积压缓冲用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。在了解相应功能之前,不需要知道这些缓冲的细节;这部分内存由jemalloc分配,因此会统计在used_memory中。
- 内存碎片
内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致redis释放的空间在物理内存中并没有释放,但redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。
Redis还提供的高级工具像慢查询分析、性能测试、Pipeline、事务、Lua自定义命令、Bitmaps、HyperLogLog、发布/订阅、Geo等个性化功能。
Redis缓存和数据库间数据一致性问题高并发的业务场景下,经常使用redis做一个缓冲操作,先读取缓存,如果缓存不存在,则读取数据库。
获取数据不会存在太大问题,但是写入的时候,就可能出现数据不一致的情况。因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。
第一种方案:采用延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
具体的步骤就是
先删除缓存,再写数据库,休眠500毫秒,再次删除缓存。
具体该休眠多久,需要评估自己的项目的读数据业务逻辑的耗时。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。
设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
该方案的弊端
结合双删策略 缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
第二种方案:异步更新缓存(基于订阅binlog的同步机制)
技术整体思路:
MySQL binlog增量订阅消费 消息队列 增量数据更新到redis
- 读Redis:热数据基本都在Redis
- 写MySQL:增删改都是操作MySQL
- 更新Redis数据:MySQ的数据操作binlog,来更新到Redis
增量,指的是mysql的update、insert、delate变更数据。
Redis更新
一个是全量(将全部数据一次写入到redis)一个是增量(实时更新)
一旦MySQL中产生了新的写入、更新、删除等操作,就可以把解析的binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。类似MySQL的主从备份机制。
例如:canal是阿里巴巴旗下的一款开源项目,MySQL binlog 增量订阅&消费组件,目前主要支持了MySQL(也支持mariaDB)。
redis通讯协议Redis客户端和服务端之间使用一种名为RESP(REdis Serialization Protocol)的二进制安全文本协议进行通信。RESP设计的十分精巧,下面是一张完备的协议描述图:
img
Redis中海量数据操作注意事项Redis是单线程服务,所有指令都是顺序执行,当某一指令耗时很长时,就会阻塞后续的指令执行。例如大key,大value,keys操作等。
禁用/重命名危险命令
例如:keys会在大量key遍历时,比较耗时,会阻塞后续的指令执行,严重时导致Redis实例崩溃甚至服务器宕机。利用SCAN系列命令完成数据迭代。同时禁用keys命令。
# 禁用key rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command CONFIG "" rename-command KEYS ""
大key
大key一般是指value值大别大的key。
- redis 是单线程,操作bigkey比较耗时,尤其是使用hgetall、lrange、get、hmget 等操作时, 内存空间不平衡,超时阻塞。每次获取 bigKey 的网络流量较大。严重时会导致应用程序连不上,实例或者集群在某些时间段内不可用的状态。
- 如果是集群模式下,bigkey无法做到负载均衡,导致请求倾斜到某个实例上,导致实例的QPS会比较大,内存占用也较多。给对应实例带来瓶颈和影响。
- bigkey进行删除操作,如果直接进行DEL 操作,被操作的实例会被Block住,导致无法响应应用的请求,而这个Block的时间会随着key的变大而变长。
处理方案:
- 单个key存储大value:只存取必要数据数据压缩(应用程序需要自己处理序列化问题)大value拆分为多个key存储。真实数据存储在第三方DB,例如:大的数据报文可以存储在mongo中,redis只负责存储数据标识。
- hash,set,zset,list中存储过多数据大key拆分,类似分表操作。例如:哈希取模 。可以减少key膨胀。
- 海量key存储选择合适的淘汰策略value小,key多,数据存在关联性。可以进行value值合并操作。或则采用其他结构,例如hash结构。
暂时想这么多。
redis安全禁止一些高危命令
rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command CONFIG "" rename-command KEYS ""
禁止root用户启动redis
为 Redis 服务创建单独的用户和家目录,并且配置禁止登陆。
添加密码验证,同时配置文件权限修改。
redis效率高,简单的密码容易为攻击者暴破。密码要设置复杂。
requirepass mypassword
chmod 600 /etc/redis/redis.conf #redis配置文件
禁止外网访问 Redis
bind 127.0.0.1
修改默认6379端口
port 6666
默认端口为6379。
内网运行,尽量避免有公网访问
对于各种内网代理要进行风险评估,不要随意搭建。防止攻击。
监控redis状态
Redis 中设置过期时间主要通过以下四种方式Redis 过期删除策略
EXPIRE <KEY> <TTL> : 将键的生存时间设为 ttl 秒 PEXPIRE <KEY> <TTL> :将键的生存时间设为 ttl 毫秒 EXPIREAT <KEY> <timestamp> :将键的过期时间设为 timestamp 所指定的秒数时间戳 PEXPIREAT <KEY> <timestamp>: 将键的过期时间设为 timestamp 所指定的毫秒数时间戳.
定期删除
redis会把设置了过期时间的key放在单独的字典中,每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key的操作。
由于算法采用的随机取key判断是否过期的方式,故几乎不可能清理完所有的过期Key;调高hz参数可以提升清理的频率,过期key可以更及时的被删除,但hz太高会增加CPU时间的消耗,为了保证不会循环过度,导致卡顿,扫描时间上限默认不超过25ms。
系统中应避免大量的key同时过期,给要过期的key设置一个随机范围。
惰性删除
过期的key并不一定会马上删除,还会占用着内存。 当你真正查询这个key时,redis会检查一下,这个设置了过期时间的key是否过期了? 如果过期了就会删除,返回空。
Redis采用的过期策略
惰性删除 定期删除
Redis 内存淘汰策略
- FIFO:First In First Out,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。
- LRU:Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。
- LFU:Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。
- RANDOM: 随机删除
noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键(默认策略,不建议使用) allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键 volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键(不建议使用) allkeys-random:加入键的时候如果过限,从所有key随机删除 volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐(不建议使用) volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键 volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键 allkeys-lfu:从所有键中驱逐使用频率最少的键
参考:
Redis - 缓存雪崩、缓存击穿、缓存穿透https://segmentfault.com/a/1190000022349593
Redis分布式锁http://www.redis.cn/topics/distlock.html
高并发架构系列:Redis缓存和MySQL数据一致性方案详解https://www.jianshu.com/p/b28fb9d5acb7
Redis 键的过期删除策略及缓存淘汰策略https://www.cnblogs.com/rinack/p/11549362.html
,