Redis总结

总体架构

Redis Cluster采用无中心结构,每个节点都保存数据和整个集群的状态
每个节点都和其他所有节点连接,这些连接保持活跃
使用gossip协议传播信息以及发现新节点
redis节点不作为client请求的代理,client根据节点返回的信息重定向请求

在这里插入图片描述

redis是【单线程结构】,避免了并发对数据结构加锁引起的额外性能损耗。而且redis的性能瓶颈是在网络上,单线程就够用。Redis采取纯内存操作,采用非阻塞IO多路复用(操作系统提供select, epoll, evport, kqueue等机制)。redis使用C使用了linux的epoll技术。

单线程的核心底层就是【事件驱动】

  • 文件事件(file event):Redis服务器通过套接字与客户端(或其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作;
  • 时间事件(time event):Redis服务器的一些操作(比如serverCron函数)需要在给定的时间执行,而时间事件就是服务器对这类定时操作的抽象;

持久化方式

Redis 提供了多种不同级别的持久化方式:

  • RDB持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。RDB模式可能需要调整Linux的内核参数。这主要是因为fork子进程时,需要申请巨大的内存空间(大小和父进程一致),但是由于linux的写时复制机制,这些内存实际上都不需要被使用。所以内存不会被使用。但是内核不知道这件事情,会因为该内核参数向linux进程say no。
    https://blog.csdn.net/houjixin/article/details/46412557
    RDB生成的文件为dump.rdb。如果不作处理,下一次会redis先建立一个临时文件,然后将其替换。

  • AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。

显然AOF更不容易丢失数据(一般认为最多只丢失最后一秒的数据),但是慢,占用空间大,。AOF文件也可以设置为同步更新到磁盘,但是这样速度太慢,一般不推荐。

Redis中的数据结构

Redis中包含5种数据类型:STRING、LIST、SET、HASH、ZSET。
5种数据结构最终存储的数据类型实际只有两种:字符和数值,Redis能够区分存储的值是字符还是数字;
key只能是STRING,不要使用特殊字符。

  • LIST的底层实现是ziplist或者linkedlist。
  • SET对象的编码可以是intset或者hashtable。
  • HASH的底层实现可以是ziplist或者hashtable。
  • ZSET使用跳跃表+dict或者是ziplist。

这五种数据结构的存储方式实际上非常重要,帮助redis在内存消耗和效率上达到平衡。

ziplist可以参考如下文章
https://zhuanlan.zhihu.com/p/144211926?from_voters_page=true
intset可以参考如下文章
https://blog.csdn.net/yangbodong22011/article/details/78671625

一致性HASH算法

最关键的区别就是,对节点和数据,都做一次哈希运算,然后比较节点和数据的哈希值,数据取和节点最相近的节点做为存放节点。这样就保证当节点增加或者减少的时候,影响的数据最少。

先构造一个长度为2的32次方的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 2的32次方-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值计算得到其Hash值(其分布也为[0, 2的32次方-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。

一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据(即本该映射到该台服务器上的数据被映射到了下一台),其它不会受到影响。

除了用于缓存,一致性hash算法也用于ngnix的负载均衡中。然而,REDIS并没有使用一致性hash算法

一致性HASH有如下缺点:

  • 比如说有Hash环上有A、B、C三个服务器节点,分别有100个请求会被路由到相应服务器上。现在在A与B之间增加了一个节点D,这导致了原来会路由到B上的部分节点被路由到了D上,这样A、C上被路由到的请求明显多于B、D上的,原来三个服务器节点上均衡的负载被打破了。某种程度上来说,这失去了负载均衡的意义,因为负载均衡的目的本身就是为了使得目标服务器均分所有的请求。(解决办法:解决这个问题的办法是引入虚拟节点,其工作原理是:将一个物理节点拆分为多个虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布在Hash环上。采取这样的方式,就可以有效地解决增加或减少节点时候的负载不均衡的问题。)
  • 为了解决上述问题,需要上千个虚拟节点。这就让客户端完成key到节点的映射难度很大。最合理的做法是采取查找树。

HASH槽

一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,

集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和。

例如当前集群有3个节点,槽默认是平均分的:
节点 A (6381)包含 0 到 5499号哈希槽.
节点 B (6382)包含5500 到 10999 号哈希槽.
节点 C (6383)包含11000 到 16383号哈希槽.
当新增或者减少节点时,调整槽的分布即可。虽然影响的数据比一致性HASH多,但是大大减少了数据管理的成本。

负载均衡

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

“事务”

【号称事务,但是实际上不保证原子性】
【redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令】
它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令。
事务可以理解为一个打包的批量执行脚本。这个批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

事务中的常见命令:

  1. MULTI:使用该命令,标记一个事务块的开始,通常在执行之后会回复OK,(但不一定真的OK),这个时候用户可以输入多个操作来代替逐条操作,redis会将这些操作放入队列中。

  2. EXEC:执行这个事务内的所有命令。

  3. DISCARD:放弃事务,即该事务内的所有命令都将取消。当你开始一个MULTI后,必须用一个MULTI和EXEC作为结束

  4. WATCH:监控一个或者多个key,如果这些key在提交事务(EXEC)之前被其他用户修改过,那么事务将执行失败,需要重新获取最新数据重头操作(类似于乐观锁)。

  5. UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。

REDIS事务的特点

  • 不支持回滚!不保证隔离型!事实上,由于REDIS指令串行执行,如果一个事务脚本的所有命令全部在一个server上,也就根本没有什么隔离一说,手动回滚也没问题。但是,你需要保证一个事务脚本的所有命令全部在一个server上

Redis用作消息队列

第一种方案,利用redis的list数据结构。生产者lpush,消费者brpop。
brpop是RPOP命令的阻塞版本: 命令会以从左到右的顺序,访问给定的各个列表,并弹出首个非空列表最右端的项; 如果所有给定列表都为空,那么客户端将被阻塞,直到等待超时,或者有可弹出的项出现为 止;
设置 timeout参数为0表示永远阻塞。

第二种方案,利用redis的发布订阅机制,生产者发布,消费者订阅。但是这个一旦有网络抖动等问题就会丢数,仅使用于日志或者统计。

redis的消息队列没有ACK机制。处理失败要记得手动放回队列。

REDIS用做分布式锁

乐观锁
  • 乐观锁:其实说白了,就是好比一个健身房里只有一台跑步机,在健身房门口有个排号机,每个进健身房的人都得先领一个号码才能进入,如果跑步机上有人,则在一边做做热身、喝喝水,如果跑步机上没人,则确认跑步机上当前显示的号码(上一个用过跑步机的人的号码)是否比自己手持的小,如果小,则可以使用;否则,就意味着过号,而过号在现实中我们的都知道要么走,要么重排,就是不能插队,在系统中也是一样的,通常是返回错误。乐观锁往往用于秒杀抢红包等场景。

https://www.cnblogs.com/cww0814/p/7805854.html

乐观锁的一种方式是利用数据库。数据表中存储数据版本的字段(也就是上一个用过跑步机的人的号码)。我们也可以利用REDIS中的WATCH和事务来实现这个问题。以描述系统为例,首先先WATCH这个KEY(库存),然后读取KEY值,然后MULTI 开启一个事务,修改KEY值(库存-1),然后EXEC触发一个事务。如果期间库存被别人修改,那么就就没有抢到乐观锁。

  1. multi,开启Redis的事务,置客户端为事务态。
  2. exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态。
  3. discard,取消事务,置客户端为非事务态。
  4. watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。

下面是一个10件商品的秒杀流程。


public class MyRunnable implements Runnable {
 
    String watchkeys = "watchkeys";// 监视keys
    Jedis jedis = new Jedis("192.168.3.202", 6379);
 
    public MyRunnable() {
    }
 
    @Override
    public void run() {
        try {
            jedis.watch("watchkeys");// watchkeys //添加watch
 
            String val = jedis.get(watchkeys);//获取当前已经成功的人数
            int valint = Integer.valueOf(val);
            String userifo = UUID.randomUUID().toString();
            if (valint < 10) {
                Transaction tx = jedis.multi();// 开启事务
 
                tx.incr("watchkeys");
 
                List<Object> list = tx.exec();// 提交事务,如果此时watchkeys被改动了,则返回null
                if (list != null) {
                    System.out.println("用户:" + userifo + "抢购成功,当前抢购成功人数:"
                            + (valint + 1));
                    /* 抢购成功业务逻辑 */
                    jedis.sadd("setsucc", userifo);
                } else {
                    System.out.println("用户:" + userifo + "抢购失败");
                    /* 抢购失败业务逻辑 */
                    jedis.sadd("setfail", userifo);
                }
 
            } else {
                System.out.println("用户:" + userifo + "抢购失败");
                jedis.sadd("setfail", userifo);
                // Thread.sleep(500);
                return;
            }
 
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }
 
    }
 
}

上述

1、利用redis的watch功能,监控这个redisKey的状态值
2、获取redisKey的值
3、创建redis事务
4、给这个key的值+1
5、然后去执行这个事务,如果key的值被修改过则回滚,key不+1
悲观锁

如果是极为稀缺的商品秒杀,那就只能使用悲观锁了。

  • 悲观锁:还是那个健身房。这次在门口不需要排号机了,而是挂着把钥匙(只有一把),想进去的人必须拿到这把钥匙才行,拿到钥匙的人可以进入,不管是热身、喝水还是跑步都可以,直到他出来把钥匙挂回墙上,下一个才能去争取,拿到的才可以再进去。听着好像有点不人性化,所以悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。

REDIS的悲观锁主要是通过SETNX来实现。利用REDIS的单线程特性,所谓的加锁就是SETNX向REDIS中放一个key,如果已经存在则返回0,加锁失败。返回1则加锁成功。释放锁是通过删除这个KEY。

        System.out.println("_____________begin______________");
        RLock rLock1= redissonClient.getLock("GDL_HAHA");
        System.out.println(rLock1);
        RLock rLock2 = redissonClient.getLock("GDL_HAHAA");//注意,如果锁的名称相同,虽然getlock返回的是相同的对象,但是实际上是一把锁
        System.out.println(rLock2);
        boolean locked = rLock1.tryLock();
        System.out.println(rLock1.isLocked());
        System.out.println(rLock2.isLocked());
        System.out.println("_____________end______________");

如下代码会产生异常

    @Test
    public void testParallelRedisLock(){

        System.out.println("_____________begin______________");
        redissonClient.getLock("GDL_TEST");
        new Thread(new Runnable() {
            @Override
            public void run() {
                RLock rLock = redissonClient.getLock("GDL_HAHA");
                rLock.tryLock();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                RLock rLock= redissonClient.getLock("GDL_HAHA");
                rLock.unlock();
            }
        }).start();
    }

Exception in thread "Thread-5" java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: c1493d67-873a-462f-b26e-4c63dd51736b thread-id: 37
	at org.redisson.RedissonLock.unlock(RedissonLock.java:366)

使用Redis分布式锁的的基本代码是

       String lockKey = key + LOCK_KEY_TAIL;
        RLock lock = redissonClient.getLock(lockKey);
        boolean locked = false;
        try {
            locked = lock.tryLock(15000, 7500, TimeUnit.MILLISECONDS);
            //Do some job here
        } catch (Exception e) {
            log.error("updateRedis Exception key: {} ,reason ,{}", key, reason);
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {// 必须要加上判断是否被当前线程所有,不然超时释放锁后会产生IllegalMonitorStateException异常
                lock.unlock();
            }
        }

在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。如果不想等待redis中的内容超时的话,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。

1. C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,获得foo.lock的时间戳,通过比对时间戳,发现锁超时。
2. C2 向foo.lock发送DEL命令。
3. C2 向foo.lock发送SETNX获取锁。
4. C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。
5. C3 向foo.lock发送SETNX获取锁。

所以这样做是不行的。如果锁超时,那我们必须要等待redis自动超时。但是这样也不行。因为SETNX和EXPIRE(设置超时时间)是两条redis指令【阿里内部的Tair就解决了这个问题】,你不能保证自己的系统在这两条命令之间不崩溃。
采用过期时间的另外一个问题是,如果一个系统在抢到锁执行执行业务逻辑时间过长,超过了过期时间,在它释放锁的时候,你如何防止它不释放别的线程锁呢?
所以这种方法是绝对不可行的。非常容易出现超卖。

所以我们加锁的时候,其实必须在redis值中表示这是具体哪个线程加的锁。在我们释放锁的时候,要用LUA脚本保证compare And delete的逻辑。其他线程在检测到锁超时时,必须按照对应的value相等才能删,防止误删其他线程刚刚假的锁。

还有一种方法,就是利用getset命令。将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。

进程P4执行 SETNX lock.foo 以尝试获取锁
由于进程P1已获得了锁,所以P4执行 SETNX lock.foo 返回0,即获取锁失败
P4执行 GET lock.foo 来检测锁是否已超时,如果没超时,则等待一段时间,再次检测
如果P4检测到锁已超时,即当前的时间大于键 lock.foo 的值,P4会执行以下操作 
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
由于 GETSET 操作在设置键的值的同时,还会返回键的旧值,通过比较键 lock.foo 的旧值是否小于当前时间,可以判断进程是否已获得锁
假如另一个进程P5也检测到锁已超时,并在P4之前执行了 GETSET 操作,那么P4的 GETSET 操作返回的是一个大于当前时间的时间戳,这样P4就不会获得锁而继续等待。注意到,即使P4接下来将键 lock.foo 的值设置了比P5设置的更大的值也没影响。

其实利用利用redis的原子性,我们有更简单的办法。就是把库存作为一个值塞到redis里。然后

Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);

然后每次访问对库存-1就行。但是,这虽然可能不会超卖,但是可能会发生少卖。就是在获取redis锁后仍然没有成功交易的情况。

使用REDIS做分布式自增ID

比如说淘宝的订单号,肯定不能相同,而且也应该保持一个大致的顺序。REDIS自增操作是原子性的。这个可以让我们轻松搞定这一问题。但是!redis的同一个key只存在一个server(或者一个主从)里,所以可能会有性能问题。可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
事实上,采用时间+UUID的做法也是可以的。

使用REDIS可能遇到的问题

缓存击穿

黑客故意取请求缓存中不存在的数据,导致所有请求都会被堆积到数据库上,引起数据库瘫痪。可以采取异步起一个线程读数据库,预热和更新缓存。也可以采取布隆滤波器,迅速判断出请求所携带的key是否合法。但是这两种方法都有局限性,所以还是建议采取封IP等前端的反DDOS措施。

缓存雪崩

同一时间缓存大面积失效。假设你是批量把数据库中的内容更新到缓存中,并设置了相同的缓存时间,那么到时后,缓存将集体失效,所有请求瞬间全部打入数据库,这显然不合理。建议设置随机的缓存时间。

缓存一致性

缓存一致性是一个非常复杂的问题。议采取先更新数据库,再删除redis缓存的方法。

具体使用

redis在java中可以使用jedis,也可以使用spring封装的jedis(Spring Data Redis)

Redis与Python

主要有以下两个包

[kettle@vm-kvm11559-app bin]$ ./pip list|grep -i redis
redis (2.10.5)
[kettle@vm-kvm11559-app bin]$ ./pip show redis
---
Metadata-Version: 1.1
Name: redis
Version: 2.10.5
Summary: Python client for Redis key-value store
Home-page: http://github.com/andymccurdy/redis-py
Author: Andy McCurdy
Author-email: sedrik@gmail.com
License: MIT
Location: /DATA/software/anaconda2/lib/python2.7/site-packages
Requires:
Classifiers:
  Development Status :: 5 - Production/Stable
  Environment :: Console
  Intended Audience :: Developers
  License :: OSI Approved :: MIT License
  Operating System :: OS Independent
  Programming Language :: Python
  Programming Language :: Python :: 2.6
  Programming Language :: Python :: 2.7
  Programming Language :: Python :: 3
  Programming Language :: Python :: 3.2
  Programming Language :: Python :: 3.3

Redis实现细节之ZipList

ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。

实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。

另外,ziplist为了在细节上节省内存,对于值的存储采用了变长的编码方式,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。我们接下来很快就会讨论到这些实现细节。

在这里插入图片描述

  • zlbytes: ziplist的长度(单位: 字节),是一个32位无符号整数
  • zltail: ziplist最后一个节点的偏移量,反向遍历ziplist或者pop尾部节点的时候有用。
  • zllen: ziplist的节点(entry)个数
  • entry: 节点
  • zlend: 值为0xFF,用于标记ziplist的结尾

entry的结构如下
在这里插入图片描述

  • prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist
    • entry的前8位小于254,则这8位就表示上一个节点的长度
    • entry的前8位等于254,则意味着上一个节点的长度无法用8位表示,后面32位才是真实的prevlength。用254 不用255(11111111)作为分界是因为255是zlend的值,它用于判断ziplist是否到达尾部。
  • encoding: 当前节点的编码规则,下文会详细说
  • data: 当前节点的值,可以是数字或字符串
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值