redis的那些事儿

近期接触一个框架,架构体系是java(spring boot微服务)、mysql、redis。 听说这个套框架能支持千万级别的访问,具体能支持多少我也没有详细的测试过,先说说他这套架构是怎么存储的,mysql的做用是作数据永存,全部的查询全是redis,这速度确定比直接操做mysql,毕竟不是一个级别的,以前也常常用redis,可是都是用的普通的东西,今天就详细了解下redis,补充补充redis知识点。html

开始以前先了解下为啥redis比mysql快,简单说一下。。。。。(算了仍是别说了,若是这个都不知道,那你也不必往下看了)java

1.首先,redis数据格式:

①Stringmysql

    能够是字符串,整数或者浮点数,对整个字符串或者字符串中的一部分执行操做,对整个整数或者浮点执行自增(increment)或者自减(decrement)操做。git

②list(列表)github

    一个链表,链表上的每一个节点都包含了一个字符串,虫链表的两端推入或者弹出元素,根据偏移量对链表进行修剪(trim),读取单个或者多个元素,根据值查找或者移除元素。面试

③set(集合)redis

    包含字符串的无序收集器(unordered collection)、而且被包含的每一个字符串都是独一无二的。添加,获取,移除单个元素,检查一个元素是否存在于集合中,计算交集,并集,差集,从集合里面随机获取元素。算法

④hash(散列spring

包含键值对无序散列表,添加,获取,移除当键值对,获取全部键值对。sql

⑤zset(有序集合

    字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定。添加,获取,删除单个元素,根据分值范围(range)或者成员来获取元素。

(特别注意:zset是有序集合,可是尽可能少用,这个速度确定不如无序的,具体缘由,本身去百度下吧)

2.HyperLogLog

Redis 在 2.8.9 版本添加了 HyperLogLog 结构。Redis HyperLogLog是一种使用随机化的算法,以少许内存提供集合中惟一元素数量的近似值。HyperLogLog 能够接受多个元素做为输入,并给出输入元素的基数估算值:

基数:集合中不一样元素的数量。

好比:

{‘a’, ‘b’, ‘c’, ‘b’, ‘a’} 的基数就是3。(注:a、b、c)

{1, 3, 5, 7, 5, 7, 8} 的基数就是5。(注:一、三、五、七、8)
估算值:算法给出的基数并非精确的,可能会比实际稍微多一些或者稍微少一些,但会控制在合理的范围以内。 
HyperLogLog 的优势是,即便输入元素的数量或者体积很是很是大,计算基数所需的空间老是固定的、而且是很小的。

在 Redis 里面,每一个 HyperLogLog 键只须要花费 12 KB 内存,就能够计算接近 2^64 个不一样元素的基数。这和计算基数时,元素越多耗费内存就越多的集合造成鲜明对比。

可是,由于 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素自己,因此 
HyperLogLog 不能像集合那样,返回输入的各个元素。

3.GEO

Redis3.2版本提供了GEO功能,支持存储地理位置信息用来实现诸如摇一摇,附近位置这类依赖于地理位置信息的功能。

①geoadd:增长某个地理位置的坐标;

②geopos:获取某个地理位置的坐标;

③geodist:获取两个地理位置的距离;

④georadius:根据给定地理位置坐标获取指定范围内的地理位置集合;

⑤georadiusbymember:根据给定地理位置获取指定范围内的地理位置集合;

⑥geohash:获取某个地理位置的geohash值。

4.Pub/Sub

"发布/订阅"在redis中,被设计的很是轻量级和简洁,它作到了消息的“发布”和“订阅”的基本能力;可是还没有提供关于消息的持久化等各类企业级的特性。

一个Redis client发布消息,其余多个redis client订阅消息,发布的消息“即发即失”,redis 不会持久保存发布的消息;消息订阅者也将只能获得订阅以后的消息,通道中此前的消息将无 从得到。

消息发布者,即publish客户端,无需独占连接,你能够在publish消息的同时,使用同一个redis-client连接进行其余操做(例如:INCR等) 消息订阅者,即subscribe客户端,须要独占连接,即进行subscribe期间,redis-client没法穿插其余操做, 此时client以阻塞的方式等待“publish端”的消息;

所以这里subscribe端须要使用单独的连接,甚至须要在额外的线程中使用。 Tcp默认链接时间固定,若是在这时间内sub端没有接收到pub端消息,或pub端没有消息产生,sub端的链接都会被强制回收, 这里就须要使用特殊手段解决,用定时器来模拟pub和sub之间的保活机制,定时器时间不能超过TCP最大链接时间,具体根据机器环境来定;

一旦subscribe端断开连接,将会失去部分消息,即连接失效期间的消息将会丢失,因此这里就须要考虑到借助redis的list来持久化; 若是你很是关注每一个消息,那么你应该基于Redis作一些额外的补充工做,若是你指望订阅是持久的,那么以下的设计思路能够借鉴:

1) subscribe端: 首先向一个Set集合中增长“订阅者ID”, 此Set集合保存了“活跃订阅”者, 订阅者ID标记每一个惟一的订阅者,此Set为 "活跃订阅者集合"

2) subcribe端开启订阅操做,并基于Redis建立一个以 "订阅者ID" 为KEY的LIST数据结构, 此LIST中存储了全部的还没有消费的消息,此List称为 "订阅者消息队列"

3) publish端: 每发布一条消息以后,publish端都须要遍历 "活跃订阅者集合",并依次 向每一个 "订阅者消息队列" 尾部追加这次发布的消息.

4) 到此为止,咱们能够基本保证,发布的每一条消息,都会持久保存在每一个 "订阅者消息队列" 中.

5) subscribe端,每收到一个订阅消息,在消费以后,必须删除本身的 "订阅者消息队列" 头部的一条记录.

6) subscribe端启动时,若是发现本身的 "订阅者消息队列" 有残存记录, 那么将会首先消费这些记录,而后再去订阅.

(注意:在消费者下线的状况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。)

5.Redis Module

BloomFilter(去重,https://github.com/wxisme/bloomfilter详细的代码例子)

RedisSearch(https://my.oschina.net/u/1858920/blog/1862825)

Redis-ML

(注这个几个我也不是很了解,只是知道这几个点)

6.redis分布式锁的实现方式

实现需求:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即便有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其余客户端能加锁。
  3. 具备容错性。只要大部分的Redis节点正常运行,客户端就能够加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端本身不能把别人加的锁给解了。

首先咱们要经过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

加锁代码

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

代码解释:

  • 第一个为key,咱们使用key来当锁,由于key是惟一的。

  • 第二个为value,咱们传的是requestId,不少童鞋可能不明白,有key做为锁不就够了吗,为何还要用到value?缘由就是咱们在上面讲到可靠性时,分布式锁要知足第四个条件解铃还须系铃人,经过给value赋值为requestId,咱们就知道这把锁是哪一个请求加的了,在解锁的时候就能够有依据。requestId可使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数咱们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,咱们进行set操做;若key已经存在,则不作任何操做;

  • 第四个为expx,这个参数咱们传的是PX,意思是咱们要给这个key加一个过时的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,表明key的过时时间。

解锁代码

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

能够看到,咱们解锁只须要两行代码就搞定了!第一行代码,咱们写了一个简单的Lua脚本代码,上一次见到这个编程语言仍是在《黑客与画家》里,没想到此次竟然用上了。第二行代码,咱们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,若是相等则删除锁(解锁)。那么为何要使用Lua语言来实现呢?由于要确保上述操做是原子性的。关于非原子性会带来什么问题,能够阅读【解锁代码-错误示例2】 。那么为何执行eval()方法能够确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来讲,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,而且直到eval命令执行完成,Redis才会执行其余命令。

注:还有简单的SETNX(能够设置过时时间)锁

7.keys和scan

keys查找全部符合给定模式 pattern 的 key 。

KEYS * 匹配数据库中全部 key 。

KEYS h?llo 匹配 hello , hallo 和 hxllo 等。

KEYS h*llo 匹配 hllo 和 heeeeello 等。

KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。

特殊符号用 \ 隔开

KEYS 的速度很是快,但在一个大的数据库中使用它仍然可能形成性能问题,若是你须要从一个数据集中查找特定的 key ,你最好仍是用 Redis 的集合结构(set)来代替。

可用版本:

>= 1.0.0

时间复杂度:

O(N), N 为数据库中 key 的数量。

返回值:

符合给定模式的 key 列表。

 

SCAN cursor [MATCH pattern] [COUNT count]

SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):

  • SCAN 命令用于迭代当前数据库中的数据库键。
  • SSCAN 命令用于迭代集合键中的元素。
  • HSCAN 命令用于迭代哈希键中的键值对。
  • ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

以上列出的四个命令都支持增量式迭代, 它们每次执行都只会返回少许元素, 因此这些命令能够用于生产环境, 而不会出现像 KEYS命令、 SMEMBERS 命令带来的问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。

不过, 增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令能够返回集合键当前包含的全部元素, 可是对于 SCAN 这类增量式迭代命令来讲, 由于在对键进行增量式迭代的过程当中, 键可能会被修改, 因此增量式迭代命令只能对被返回的元素提供有限的保证 (offer limited guarantees about the returned elements)。

由于 SCAN 、 SSCAN 、 HSCAN 和 ZSCAN 四个命令的工做方式都很是类似, 因此这个文档会一并介绍这四个命令, 可是要记住:

  • SSCAN 命令、 HSCAN 命令和 ZSCAN 命令的第一个参数老是一个数据库键。
  • 而 SCAN 命令则不须要在第一个参数提供任何数据库键 —— 由于它迭代的是当前数据库中的全部数据库键。

scan 0 默认返回10条数据。

127.0.0.1:6379> scan 0
1) "81920"
2)  1) "CMD:1000004739:4"
    2) "CMD:1000010475:2"
    3) "CMD:380071400001208:766"
    4) "CMD:1000006866:LIST"
    5) "CMD:380071400001208:20415"
    6) "CMD:380071400001231:21530"
    7) "CMD:380071400001208:21780"
    8) "CMD:7485630165:LIST"
    9) "CMD:1000001545:2"
   10) "CMD:380071400001231:4387"

 

能够用count 参数指定返回数据量:

127.0.0.1:6379> scan 0 count 100
1) "104448"
2)   1) "CMD:1000004739:4"
     2) "CMD:1000010475:2"
     3) "CMD:380071400001208:766"
     4) "CMD:1000006866:LIST"
     5) "CMD:380071400001208:20415"
     6) "CMD:380071400001231:21530"
     7) "CMD:380071400001208:21780"
     8) "CMD:7485630165:LIST"
     9) "CMD:1000001545:2"
    10) "CMD:380071400001231:4387"
    ......
    94) "CMD:201610200062:6"
    95) "CMD:VF3748211006:3"
    96) "CMD:1000009121:4"
    97) "CMD:380071400001231:6563"
    98) "CMD:1000010252:ID"
    99) "CMD:1000005261:5"
   100) "SERVER:45568_0"

 

使用match 参数来匹配模式:

127.0.0.1:6379> scan 0 match CMD* count 100
1) "104448"
2)  1) "CMD:1000004739:4"
    2) "CMD:1000010475:2"
    3) "CMD:380071400001208:766"
    4) "CMD:1000006866:LIST"
    5) "CMD:380071400001208:20415"
    6) "CMD:380071400001231:21530"
    7) "CMD:380071400001208:21780"
    8) "CMD:7485630165:LIST"
    9) "CMD:1000001545:2"
   10) "CMD:380071400001231:4387"
   ......
   86) "CMD:201610200062:6"
   87) "CMD:VF3748211006:3"
   88) "CMD:1000009121:4"
   89) "CMD:380071400001231:6563"
   90) "CMD:1000010252:ID"
   91) "CMD:1000005261:5"

最重要的是scan不会阻塞服务器,现网环境也能够用,真方便。

8.使用redis一些小的注意

①使用list结构做为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

②list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

③redis如何实现延时队列:使用sortedset,拿时间戳做为score,消息内容做为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒以前的数据轮询进行处理。

④若是大量的key过时时间设置的过于集中,到过时的那个时间点,redis可能会出现短暂的卡顿现象。通常须要在时间上加一个随机值,使得过时时间分散一些。

⑤bgsave作镜像全量持久化,aof作增量持久化。由于bgsave会耗费较长时间,不够实时,在停机的时候会致使大量丢失数据,因此须要aof来配合使用。在redis实例重启时,优先使用aof来恢复内存的状态,若是没有aof日志,就会使用rdb文件来恢复。

⑥aof文件过大恢复时间过长怎么办?你告诉面试官,Redis会按期作aof重写,压缩aof文件日志大小。

⑦Redis4.0以后有了混合持久化的功能,将bgsave的全量和aof的增量作了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。

⑧若是忽然机器掉电会怎样?取决于aof日志sync属性的配置,若是不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。可是在高性能的要求下每次都sync是不现实的,通常都使用定时sync,好比1s1次,这个时候最多就会丢失1s的数据。

⑨Pipeline有什么好处,为何要用pipeline?

能够将屡次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候能够发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

⑩Redis的同步机制

Redis可使用主从同步,从从同步。第一次同步时,主节点作一次bgsave,并同时将后续修改操做记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操做记录同步到复制节点进行重放就完成了同步过程。

⑪集群的原理是什么?

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提高为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

相关文章
相关标签/搜索