redis的基本功能(redis常见问题二)(1)

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为什么这么快?
  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 使用多路I/O复用模型,非阻塞IO;多指的是多个网络连接,复用指的是复用同一个线程
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis内存划分
  • 数据

作为数据库,数据是最主要的部分;这部分占用的内存会统计在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设计的十分精巧,下面是一张完备的协议描述图:

redis的基本功能(redis常见问题二)(2)

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 中设置过期时间主要通过以下四种方式

EXPIRE <KEY> <TTL> : 将键的生存时间设为 ttl 秒 PEXPIRE <KEY> <TTL> :将键的生存时间设为 ttl 毫秒 EXPIREAT <KEY> <timestamp> :将键的过期时间设为 timestamp 所指定的秒数时间戳 PEXPIREAT <KEY> <timestamp>: 将键的过期时间设为 timestamp 所指定的毫秒数时间戳.

Redis 过期删除策略

定期删除

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

,