聊聊中间件之redis

前言

不知不觉4月份已经过去大半了,转眼间自己已经失业半个月了,感觉还有好多东西没有复习,不管怎么样,不能让自己闲下来,今天就来整理一下redis相关的内容

about redis

1
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。  --- wikipedia

redis是一款基于内存的数据库,即NOSQL,采用单线程模型,能支持10W的并发
常用的数据结构 String List Set ZSet Hash
持久化方式:RDB(快照),AOF(日志追加)

redis为什么高性能?

  • 单线程模型避免线程上下文切换问题
  • 纯内存操作
  • 核心基于非阻塞的IO多路复用

redis中的数据结构

string字符串 Simple Dynamic String(动态字符串)

String是redis里面最简单的key-value类型了,它的value不仅可以是String还可以是数字,它也是Memcached中的唯一的数据类型,redis String底层使用SDS(Simple Dynamic String)实现,常见的用法就是缓存、计数等功能,常用指令 set get decr(-1) incr(+1) mget(获取一个或多个给定key的值)

list 列表

list就是列表,在redis底层是用链表实现的,通过列表可以轻松实现最新消息列表等与插入序有关的业务。
常用指令 lpush(链表头插入一个元素),lpop(链表头删除一个元素),rpush,rpop,lrange(获取指定范围内的元素)

set 集合

set就是一个集合,一堆不重复值得集合,利用redis提供的set可以存储一些集合性的数据,比如微博中一个人关注的人,粉丝等等,redis还对set提供了一系列的函数可以实现对集合的并、交、差,这就可以非常方便的实现共同好友、共同关注、二度好友等功能

常用指令 sadd(向一个集合中添加一个或多个元素) spop(移除一个随机元素) srem(移除一个或多个指定元素) smembers(获取集合所有成员) sunion(求并集)

zset有序集合 内部实现为skip list跳跃表

zset(又称sorted set),有序集合,比set增加了一个score(权重)的概念,使得集合中的元素可以按照score进行排序,这一特性特别适合做排行榜

hash 字典

hash几乎可以等价于java中的hashmap,底层采用数组+链表的结构,常用于存储对象(因为)
常用指令 hget,hset,hgetall

redis常见的使用场景

  • 缓存
  • 单线程模型可做分布式锁
  • 排行榜 有序集合可以实现
  • 计数器 商品浏览量 视频播放数 redis 提供的incr 内存操作 性能非常好
  • 分布式会话
  • 社交网络 共同关注/共同好友 redis提供的hash和集合(交并差)

redis中的内存淘汰策略

redis中数据的存取是纯内存操作的,内存是很昂贵的资源,而redis在64位操作系统时是不做内存容量限制的,那么当数据量很大的时候,可想而知,如果不对redis中的数据进行淘汰,很有可能导致操作系统因为内存耗尽而进入假死状态,一般而言 我们会用两种机制(过期策略+内存淘汰机制)配合从而对一些不需要的数据进行删除用来节约空间。

过期策略

通常情况下,我们设置一个key的时候,可以对这个key的过期时间进行设置,当key过期后,redis通过

定时删除

定时删除是指每隔一段时间,随机抽取一些设置了过期时间的key,检测是否过期,过期了就将其删掉

key可能很多,扫描所有的key值可能会很消耗性能,这样的话有一些key可能会删不掉,这时候就需要惰性删除的配合进行删除。

惰性删除

当redis中的key过期后,redis并不会主动去删除它,而是等到客户端访问某个key的时候,判断key是否过期,如果没过期则直接返回,如果过期,redis会将这个key删除并且不会返回给客户端,这样的话可以解决一些已经过期了,但没有被定期删除扫到的key,但是又引入了另一个问题,已经过期的key并且永远不会访问到,这样既不会被定时删除所处理,也不会被惰性删除所处理,这时就要引入内存淘汰的机制了

内存淘汰机制

redis在使用内存超过某个阈值(通过配置文件进行配置 maxmemory)的时候,会触发内存淘汰机制,选取一些key进行删除

1
2
maxmemory
maxmemory-policy

配置文件这里的maxmemory-policy 就是可以配置的策略,其中可配置的策略有以下几种

  • noeviction 默认策略 不淘汰,内存不足时候,新的写入会报错
  • allkeys-lru 当内存不足的时候,在所有的键空间中移除最近最少使用的key
  • allkeys-random 在所有键中,随机移除key
  • volatile-lru 在设置了过期时间的键空间中,找最近最少使用的key进行删除
  • volatile-random 在设置了过期时间的键空间中,随机移除key
  • volatile-ttl 在设置了过期时间的键空间中,有更早过期时间的key将会被删除

一般来说 如果只是用来缓存数据的话,用allkeys-lru就可以了,如果既需要缓存又要保证数据安全性进行持久化的话,出于性能考虑推荐用volatile-lru

redis 分布式锁

单节点分布式锁

单节点的分布式锁(只有一个master节点)往往是不可靠的,虽然实现起来很简单,但是如果主节点宕机或者没有来得及同步数据,给从节点导致从节点选举为master时候锁丢失,这种方案很明显是不可靠的

setnx + del

执行业务前通过 setnx 获取锁,执行完业务删除锁,表面上看起来没什么问题,但是如果业务的执行出现了异常,再或者服务宕机没有来得及删除该锁,导致这个业务再次执行的时候再也获取不到这个锁,从而导致死锁,这样是行不通的

setnx + expire

业务先通过set nx获取锁,然后再通过expire命令为锁设置过期时间,这种情况实际上跟先加锁后删除的效果是一样的,如果业务执行出现了异常,expire没有被执行,业务还是无法获取这个锁,出现这样的问题的根本原因实际上就是因为 setnx + del || expire是两条指令,并非一个原子性操作,两个指令如果能同时执行就可以避免这样的问题

lua脚本 setnx + expire

setnx + expire 本身不是原子性的,但是redis提供了单线程模型执行lua脚本,保证脚本在运行过程中不会被任意其他请求打断,通过lua脚本执行相同的命令即可保证这两条命令的原子性

set nx ex

redis 2.6.12开始,set命令开始支持EX、NX、PX、XX等选项,可以完全替代setnx等操作,而且还是原子性的,可以执行通过set实现单节点的分布式锁。

多节点redis分布式锁

RedLock(真正意义上的分布式锁)

假设当前有5个独立的redis master节点

  • 获取当前unix时间戳 以毫秒为单位
  • 依次从5个实例尝试,使用相同的key和value获取锁,客户端在请求锁的时候,应该设置一个超时时间,防止在请求锁的时候避免由于服务端已经挂掉的时候,客户端还在死等响应结果。如果服务端没有在规定的超时时间内响应,客户端应该尽快从另外一个redis实例中获取锁
  • 客户端使用当前时间减去开始获取锁的时间,就得到锁的使用时间,并且当且仅当从大多数redis节点都获取到锁的时候,并且锁使用的时间小于锁失败时间时,锁才获取成功
  • 如果获取到了锁,锁的真正有效时间等于有效时间减去第三步获取锁所使用的时间
  • 如果获取锁失败,客户端应该在所有的redis实例上进行解锁,防止某些节点获取到锁,但是客户端没有得到响应,从而导致接下来的一端时间里客户端不能重新获取锁

但是这种分布式锁真的可靠吗? 业务时间超过了分布式锁的可用时间,这个锁还有意义吗?