likes
comments
collection
share

Redis

作者站长头像
站长
· 阅读数 9

什么是 redis ?

Redis是事件驱动程序,典型的非关系型数据库,以键值对形式存储数据,它支持的value类型有五种:String(字符串),list(列表),set(无序集合),zset(sorted set---有序集合)和hash(哈希类型集合)。Redis为了保证效率,数据都是缓存在内存中的,是存内存操作,而且是单进程,采用多路I/O复用原则,也可以持久化到磁盘上,持久化方式有两种,AOF和RDB

Redis 作用

可以用在存储限时的一些数据,验证码,频繁被访问的数据等,主要用来减少数据库的压力,缓存,也可以解决分布式锁、排行榜等

Redis 命令
//redis自带压力测试工具  redis-benchmark  100个并发请求十万次 
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000

Redis 的持久化:持久化到磁盘

有两种持久化方式:RDB和AOF

RDB:

默认的存储策略,将内存中的数据以快照的方式写入磁盘中,在 redis.conf 文件中可以修改 save 开头的配置,以达到多长时间内修改多久次进行持久化的配置,RDB 存储的数据在 dump.rdb 文件中,在哪个目录下启动 redis 服务端,该文件就会在对应目录下生成,该文件不能查看,如果需要备份,拷贝文件就行了

AOF:

AOF默认是不开启的,需要手动开启,也是在 redis.conf 文件中开启(把appendonly改为yes),就会出现一个 appendonly.aof 文件,任何操作都会保存在 appendonly.aof 文件中,可以进行查看,存储的是对 redis 的操作命令。

AOF有多中持久化策略:

策略说明
appendfsync always每次修改都进行同步,数据完整性较好
appendfsync everysec每秒同步,每秒记录数据,异步操作,如果一秒宕机,有数据丢失
appendfsync no何时同步由操作系统来决定

如果AOF和RDB同时开启,系统会默认读取AOF的数据

比较:

如果大规模的数据恢复,RDB 比 AOF 方式恢复速度要快,但是不太适应对数据完整性严格要求的情况,因为无论修改,如果在没有触发快照之前宕机了,数据就丢失了。相应的 AOF 比 RDB 安全,AOF 就比 RDB 速度慢。

数据类型

  1. String(字符串)
set key value: 					设定key持有指定的字符串value
setex key seconds value				设定值为value的key,过期时间为seconds,单位秒
psetex key milliseconds value			设定值为value的key,过期时间为seconds,单位毫秒
get key: 					获取key的value,如果value不是String类型,将返回错误信息
getset key value: 				先获取key的值,再设置key的值
incr key: 					将指定的key的value递增1,如果key不存在,则incr后的默认值为-1,如果value不能转为整形,将返回相应的错误信息
incrby key increment:		 		将指定的key的value增加increment
decrby key decrement: 				将指定的key的value减少decrement
append key value: 				在指定的key的value后面追加字符串,key不存在则创建一个key/value
setnx key value 				只有在key不存在时设置key的值为value,SETNX 是『SET if 										Not eXists』的缩写
  1. list(列表)
lpush key value1 value2... 			在指定的key所关联的list的头部插入value,key不存在则创建
rpush key value1 value2... 			在list尾部添加value
lrange key start end 	   			获取链表中从start到end的值,为-1,则为链表的倒数第一个元素
lpushx key value1 value2   			只有key存在时,在指定的key头部插入value
rpushx key value value2    			在尾部添加
lpop key 				   	返回并弹出key的第一个元素
rpop key				   	从尾部弹出并返回第一个元素
rpoplpush resource destination		   	移除resoure的尾部元素并添加到destination的头部
llen key 					返回指定的key的元素个数
lset key index value				设置列表中index位置的值为value,0为头,-1为尾
lrem key count value				删除count个值为value的元素,小于0从尾向头部遍历删除
linsert key before|after pivot valye    	在pivot元素前或后插入value
  1. set(不重复集合)
sadd key value1 value2...			向set中添加数据
smembers key					获取set中所有的成员
scard key 					获取set中成员的数量
sismember key member 				判断参数中指定的member是否存在,10
srem key member1 member2 			删除指定的成员
srandmember key					随机返回一个成员
sdiff key1 key2					返回key1和key2的差集
sinter key1 key2				返回交集
sdiffstore destination key1 key2		将key1和key2的差集存储在destination上
sinterstore destination key1 key2		将返回的交集存储在destination上。
sunion key1 key2				返回两者的并集,所有元素
  1. sorted set(有序集合)
zadd key score member score2 member2		将所有成员以及该成员的分数存储到指定key的soted-set
zcard key					返回集合中的成员数量
zcount key min max				获取分数在[min,max]的成员
zincrby key increment member			指定key的成员增加increment分
zrange key start end [withscores]		获取集合中脚标为start-end的成员,withscores表明返回的成员包含分数,从小到大,从大到小是:zrevrange
                                        
zrangebyscore key min max[withscores] [limit offset count]
						返回分数在[min,max]之间的成员按照分数从低到高
						[limit offset count]:offset,表明从脚标为offset的元素开始并返回count个成员
zrank key member				返回成员在集合中的位置
zrem key member[member…]			移除集合中指定的成员,可以指定多个成员
zscore key member				返回指定成员的分数
  1. hash(哈希)
hset key field value				为指定的key设定field/value对
hgetall key 					获取key中的所有filed-value
hget key field					返回指定key中的field的值
hmset key fields...				添加的多个field/value到key
hexists key field				判断指定key中的field是否存在
hlen key 					获取key锁包含的field的数量
hincrby key field increment			设置key中的field的值增加increment
  1. HyperLogLog
可以接受多个元素作为输入,并给出输入元素的基数估算值:

	• 基数: 集合中不同元素的数量。比如 {‘apple’, ‘banana’, ‘cherry’, ‘banana’, ‘apple’} 的基数
  	就是 3 。

	• 估算值: 算法给出的基数并不是精确的,可能会比 实际稍微多一些或者稍微少一些,但会控制在合理的范
		围之内。

	HyperLogLog 的优点是,即使输入元素的数量或者体积非常非常大,计算基数所需的空间总是固定的、并且是
很小的

  1. BitMap
	Redis提供的Bitmaps这个“数据结构”可以实现对位的操作。Bitmaps本身不是一种数据结构,实际上就是字
符串,但是它可以对字符串的位进行操作。

	可以把Bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做
偏移量。单个bitmaps的最大长度是512MB,即2^32个比特位。

GEO

Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。

Redis GEO 操作方法有:

geoadd:						添加地理位置的坐标。
geopos:						获取地理位置的坐标。
geodist:					计算两个位置之间的距离。
georadius:					根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
georadiusbymember:	根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
geohash:					返回一个或多个位置对象的 geohash 值。

key通用操作

keys patten					获取所有与patten匹配的key,*表示任意字符,?表示一个字符
del key1 key2					删除指定的key
rename key newkey				为指定的key重命名
expire key second				为当前key设置过期时间,单位秒
ttl key						查看当前key剩余过期时间
type key					查看当前key的类型
exists key					检查指定key是否存在

evalsha script key[key ...] arg[arg...]    可用版本: >= 2.6.0

script 参数是一段 Lua 5.1 脚本程序,它会被运行在 Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数。

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:

  • redis.call():在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因
  • redis.pcall():出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表( table ),用于表示错误

Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性( atomic )的方式 执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。

Redis事务

redis 的事务是一个单独的隔离操作,事务中的所有命令都会被序列化,按照顺序执行,不会被其他客户端发来的命令打断,作用就是串联多个命令,防止别的命令插队。

步骤是:

multi 开启事务,以后的命令会暂时存放在队列中,但不会执行,直到 exec 提交后开始按顺序执行,discard 用来撤销之前被暂存的命令,并不是回滚,如果 exec 之前,入队会先检查语法,命令错误(语法错误,严重错误导致服务器不能正常工作,例如内存不足)则会自动放弃事务;再如果: 事务执行 exec 之后,执行队列命令,命令执行错误并不会影响别的命令的执行,不会回滚。

multi 之后,再执行操作命令,这些命令是放在 redis 服务端的,可以使用 nc 命令确认此结果。 Redis 事务没有隔离级别的观念,开启事务只有,没有 EXEC 之前,命令并没有被真正的执行,自然也不会存在数据的更新。 使用 watch 检测指定的 key ,事务期间指定的key被改变之后,就算使用 EXEC ,队列中的命令也自动不执行。

集群模式下正常不支持,要求key必须在同一个 solt 上,可以在key中增加“{XXX}”来保证在同一个槽中

ps: 如果key包含{},就会使用第一个{}内部的字符串作为 hash key

Redis内存淘汰策略

Redis 经常作为缓存使用,难免会遇到内存空间存储瓶颈,当 Redis 内存超出物理内存限制时,内存数据就会与磁盘产生频繁交换,使 Redis 性能急剧下降。此时如何淘汰无用数据释放空间,存储新数据就变得尤为重要了。

Redis 配置参数中有一个 maxmemory 的方式来限制内存大小,当实际存储内存超过 maxmemory 参数值后,Redis就会使用内存淘汰策略,来决定如何腾出新空间。两种 lfu 是 Redis5.0 才出现的

在配置文件的maxmemory-policy后修改

淘汰策略方式
volatile-lru从已设置过期时间的数据集中挑选最近最少使用的淘汰
volatile-ttr从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random从已设置过期时间的数据集中任意挑选数据淘汰
allkeys-lru从数据集中挑选最近最少使用的淘汰
allkeys-random从数据集中挑选任意数据淘汰
volatile-lfu从已设置过期时间的数据集中挑选使用频率最低的数据淘汰
allkeys-lfu从数据集中挑选使用频率最低的数据淘汰
no-eviction禁止驱逐数据,当内存不足以容纳新入数据时,新写入操作就会报错

Redis问题

1. 缓存穿透

tomcat去Redis查询一个一定不存在的数据,由于缓存不命中,需要穿过Redis从数据库查询。

高频率缓存穿透就会有很大问题,什么情况下会出现缓存穿透?
  • 黑客攻击,黑客控制了多台客户端,发送一个一定不存在的数据,就一定会穿过Redis去查询数据库
  • 解决方案:把去Redis查询出来数据为null的这条数据,存储到redis中
  • 但是如果黑客使用UUID来请求,Redis开启了内存淘汰策略,就会适得其反
  • 解决方案:写一个过滤器,把数据库所有的id存起来,进入数据库时先判断这里有没有
  • 但是又不全是通过ID操作数据,如果把所有信息都存起来,又浪费时间和内存
  • 解决方案:redis 实现布隆算法

布隆算法:判断一个元素是否在给定集合中出现的算法,具有快速,比哈希表更节省空间,但是有一定的错误率,如果布隆算法说数据存在,那么数据可能不存在,但是说不存在,一定不存在,就是通过一定的错误率来降低内存的占用的算法。

有一个数组,里面存的全是 0 和 1,通过 n 个 hash 函数计算出指定的下标之后,把计算出的所有下标位置的数据改为 1,然后通过你传入的数据,也计算出相对应的下标,查看对应的下标是否全部是 1,如果有一个hash 函数计算出的下标的数据为 0 则是不存在

Redis底层存储是以二进制形式存储的,使用 Redis 的 setbit 命令可以修改制定位数的数据为 0 或者 1,getbit 可以获取指定位置是 0 或者 1,比如说 setbit 1E 1  就会默认把中间的所有二进制改成 0,

可以使用 redis 的 setbit 和 getbit 加上 guava 的布隆过滤器的相关方法组合使用,把 redis 的一个 key 当做一个数组,容器启动时把相关数据的 key 计算出 hash 值,把指定位置的二进制改为1,就和布隆算法一样使用。

2. 缓存击穿

一个存在的 key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿缓存,造成DB请求量大

解决方案:

  1. 设置热点数据永不过期
  2. 加锁,或者加分布式锁
3. 缓存雪崩

大量key设置过期时间,导致缓存在同一时刻全部失效,造成瞬时DB请求量大,造成缓存雪崩

解决方案:

  1. 缓存数据的过期时间随机,方式同一时间大量数据过期现象发生
  2. 分布式锁
  3. 设置热点数据永不过期
  4. 数据预热:如果数据量大,上线后运维手动触发缓存,如果数据量不大,启动自动缓存
  5. 服务限流或者接口限流

分布式锁

synchronized锁只是JVM的锁,如果使用集群,多个JVM则不能正确进行加锁,可以使用Redis创建分布式锁

加锁:

如果只用setnx,该线程执行一半宕掉,该锁就释放不了

如果简单使用setnx和expire,加锁之后,设置定时时间的时候,出现问题,则无法设置过期时间,还会死锁,需要保证写入与设置失效时间是原子性的,set key value nx px 10000 使用setnx设置值的同时设置失效时间,jedis3.1.0此方法被jedis.set(key, uuid, SetParams.setParams().nx().px(10000))替代

还要保证这个逻辑执行的慢的话,还没删除锁时,自己的锁过期了,再删除成别人的锁,设置随机字符串,比如UUID

/**
     * 非阻塞式加锁,使用setnx命令返回ok表示加锁成功,并设置失效时间与设定随机值
     */
    public boolean tryLock(String key) {
        Jedis jedis = null;
        try {
            jedis = createJedis();
            String uuid = UUID.randomUUID().toString();

            //Redis要求单个Lua脚本操作的key必须在同一个节点上,但是Cluster会将数据自动分布到不同的节点,
            //keySlot算法中,如果key包含{},就会使用第一个{}内部的字符串作为hash key,这样就可以保证拥有同样{}内部字符串的key就会拥有相同slot。
            key = "{" + key + "}";

            //使用setnx命令请求写值,并设置失效时间
            String result = jedis.set(key, uuid, SetParams.setParams().nx().px(10000));
            //OK则返回加锁成功,否则加锁失败
            if ("OK".equals(result)) {
                threadLocal.set(uuid);
                return true;
            }
            return false;
        } finally {
            returnToPool(jedis);
        }
    }

    /**
     * 阻塞式加锁,如果获取锁失败,休眠一段时间继续尝试获取锁
     */
    public void lock(String key) {
        //尝试加锁
        if (tryLock(key)) {
            return;
        }
        //加锁失败,休眠10毫秒
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //递归调用
        lock(key);
    }

解锁:

判断对应的value是否和自己手里的数据是相等的,和删除锁要是同步的

/**
     * 释放分布式锁
     * @param lockKey   锁
     */
    public void unlock(String lockKey) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        Jedis jedis = null;
        try {
            lockKey = "{" + lockKey + "}";
            jedis = createJedis();
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(threadLocal.get()));
        } finally {
            returnToPool(jedis);
        }
    }

怎么解锁时获取加锁里设置的UUID?使用ThreadLocal。

Redis主从复制

全量复制: 从节点连接到主节点后,在初始化阶段,会进行一次全量同步,主节点发出一个RDB文件给从节点,从节点接收完毕后,清除自己的旧数据,然后将 RDB 写入磁盘,紧接着从磁盘载入内存

增量复制: 全量复制之后,主服务器每执行一个写命令,就会向从服务器发送相同的写命令,从服务器接收命令并执行。

操作步骤:

把主从服务器的 redis.conf 里的 deamon 守护进程都打开,改为 yes ;从节点修改: slaveof

主从的缺点

  1. 主从复制,若主节点出现问题,则不能提供服务,需要人工修改配置将从变主

  2. 主从复制主节点的写能力单机,能力有限

  3. 单机节点的存储能力也有限

Redis哨兵模式

实现了自动化的故障恢复,缺点是写操作无法负载均衡,存储能力是单机的,有限制

  • 监控

不断的检查master和slave是否正常运行,master存货检测、master与slave运行情况监测

  • 通知(提醒)

当被监控的服务器出现问题时,向其他哨兵,客户端发送通知

  • 自动故障转移

断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的 服务器地址

哨兵模式一般是单数,因为内部有一个竞选机制,防止票数一致

配置:

修改sebtubek.conf文件里面进行配置,可控参数有:监控的主服务器;

多少个哨兵认为主服务器挂了才算挂了;

多长时间认为主服务器挂了;

同步超时时间;

几个线程开始同步新的主服务器;

数据保存的目录。

Redis集群

即使使用哨兵,redis每个实例也是全量存储,每个redis存储的内容都是完整的数据,浪费内存且有木桶效应。为了最大化利用内存,可以采用集群,就是分布式存储。即每台redis存储不同的内容,共有16384个slot。

Redis集群中内置了16384个哈希槽,当需要在Redis集群中放置一个key-value时,redis会对key进行crc16算法算出一个结果,然后结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点

这样做的好处在于可以方便的添加或移除节点。

  • 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以;
  • 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;

在这一点上,我们以后新增或移除节点的时候不用先停掉所有的 redis 服务。

Redis6 多线程

Redis 之前的版本在处理客户端请求时,都是单线程,例如清理脏数据、无用连接的释放、大 key 的删除等等会有辅助线程的存在。处理客户端请求的步骤包括:

  1. 当客户端需要与 redis 建立连接,会先经过内核,然后创建一个 socket,然后内核会监听这些 socket 套接字
  2. redis worker 工作线程会循环通过 I/O 多路复用程序获取 socket 有没有新的事件,把这些 socket 的事件取走到 worker(socket 读),不能知道哪个事件是先到内核的,所以不能保证事件的顺序性
  3. 在 work 进行执行
  4. 内容返回(socket 写)

在 6.0 之前,这些操作都是由一个 worker 线程完成的,所以被称作单线程模型。

6.X 的 Redis 默认也是不开启多线程的,可以在配置文件里开启多线程。

为什么之前不使用多线程
  • 一般的应用场景,普通 KV 存储,性能瓶颈压根不再 CPU ,而往往受到 内存网络I/O 的制约
  • Redis 中有各种类型的数据操作,甚至包含事务操作,如果使用多线程,会带来并发读写的一系列问题,增加了系统复杂度、同事可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
引入多线程

Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在4线程 IO 时性能相比单线程是几乎是翻倍了。

官方建议:至少4核的机器才开启IO多线程,并且除非真的遇到了性能瓶颈,否则不建议开启此配置 ,且配置的线程数少于机器总线程数,如果有4核建议开启2,3个线程,如果有8核建议开6线程。 线程并不是越多越好,多于8个线程意义不大。

原因

上面说到 Redis 瓶颈往往在 内存 和 网络I/O, 读写网络的 read/ write 系统调用占用了 Redis 执行期间大部分 CPU 时间,而目前 Redis 主线程只使用了一个核,所以 Redis 使用了支持多线程这一便捷的操作方式。

实现方式

上面介绍处理客户端请求的流程中说到:redis 需要通过 I/O 多路复用程序去内核中获取 socket 产生的事件,这就是网络 read 操作,执行完之后,再进行 write 操作,在进行 read/write 时,就不能进行执行操作,是阻塞的,而在遇到系统瓶颈时,大部分是这两步比较占用 Redis 性能。

Redis 的多线程,并不是多个线程进行 read、执行、write 操作,而是分以下几步:

  1. 主线程监听 I/O 多路复用器拿到所有有事件的客户端
  2. 如果未开启多线程或者任务量较少,不使用多线程直接单线程处理;否则轮询地把有事件的 client 分配给 IO 线程,由各个 IO 线程去从对应的 socket 的缓冲区里 read 并解析事件
  3. IO 线程解析完之后会把解析后的数据放到一个队列中
  4. 主线程等待所有 IO 线程工作完之后按队列里的数据进行处理
  5. 处理完之后,把客户端 client 放到一个队列中等待 IO 线程完成 write 操作

在其中有一个标识,因为这个标识的存在,IO 线程只会同时进行 read 或 write scoket,并不会存在有读的有写的情况。

总的来说就是:将 client 的输入缓冲区和把执行结果写入到 client 的输出缓冲区的过程改为了多线程的模型,而同时保证同一时间全部 IO 线程均处于读或写的状态,但是命令执行还是以单线程以队列形式来执行。