首页 > 数据库 >Redis基础

Redis基础

时间:2023-09-10 18:55:17浏览次数:47  
标签:加锁 Redis 基础 获取 分布式 节点 客户端

1.什么是Redis
  Redis是一个基于C语言开发的内存数据库,读写速度非常快,广泛应用于缓存方向。并且,Redis存储的是KV键值对数据。
  Redis内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap)。并且,Redis 还支持事务 、持久化、Lua 脚本、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。
 
2.Redis为什么这么快?
Redis 内部做了非常多的性能优化,比较重要的主要有下面 3 点:
  (1)Redis 基于内存,内存的访问速度是磁盘的上千倍;
  (2)Redis 基于Reactor模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和IO多路复用;
  (3)Redis 内置了多种优化过后的数据结构实现,性能非常高。
  Reactor模式也叫做反应器设计模式,是一种为处理服务请求并发提交到一个或者多个服务处理器的事件设计模式。当请求抵达后,通过服务处理器将这些请求采用多路分离的方式分发给相应的请求处理器。


3.分布式缓存常见的技术选型方案:
  Memcached是分布式缓存最开始兴起的那会,比较常用的。后来,随着Redis的发展,大家慢慢都转而使用更加强大的Redis了。腾讯也开源了一款类似于Redis的分布式高性能 KV存储数据库,基于知名的开源项目RocksDB作为存储引擎 ,100%兼容Redis协议和 Redis4.0所有数据模型,名为Tendis
 
4.说一下 Redis 和 Memcached 的区别和共同点
共同点 :
  都是基于内存的数据库,一般都用来当做缓存使用。
  都有过期策略。
  两者的性能都非常高。
区别 :
  (1)Redis支持更丰富的数据类型(支持更复杂的应用场景)。Redis不仅仅支持简单的k/v类型的数据,同时还提供 list,set,zset,hash等数据结构的存储。Memcached只支持最简单的k/v数据类型。
  (2)Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
  (3)Redis有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
  (4)Redis在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
  (5)Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持cluster模式的。
  (6)Memcached是多线程非阻塞 IO 复用的网络模型;Redis 使用单线程的多路IO复用模型。(Redis 6.0 引入了多线程 IO)
  (7)Redis支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言
  (8)Memcached 过期数据的删除策略只用了惰性删除,而 Redis同时使用了惰性删除与定期删除
 
4.为什么要用redis?为什么要用缓存?
(1)高性能:
  用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。如果说有一部分数据属于高频数据并且不会经常改变的话,那么就可以将这部分数据存在缓存中。
  保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
(2)高并发:
  一般MySQL这类的数据库的QPS大概都在1w左右(4核8g) ,但是使用Redis缓存之后很容易达到10w+,甚至最高能达到30w+(就单机Redis的情况,Redis集群的话会更高)。
  这样看的话,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以可以把一部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而提高系统整体的并发。
 
5.Redis除了做缓存,还能做什么?
  分布式锁:通常情况下,我们都是基于Redisson来实现分布式锁。
  限流:一般是通过 Redis + Lua 脚本的方式来实现限流。
  消息队列:Redis自带的list数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的Stream类型的数据结构更加适合用来做消息队列。它比较类似于Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
  分布式 Session:利用 string 或者 hash 保存 Session 数据,所有的服务器都可以访问。
  复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,可以很方便地完成很多复杂的业务场景,比如通过sorted set维护排行榜,通过hash存储对象数据等。

 

6.Redis 可以做消息队列么?

  Redis 2.0 之前,如果想要使用Redis来做消息队列的话,只能通过List来实现。List 实现消息队列功能太简单,像消息确认机制等功能还需要自己实现,最要命的是没有广播机制,消息也只能被消费一次。
  Redis 2.0 引入了发布订阅 (pub/sub) 功能,解决了List实现消息队列没有广播机制的问题。pub/sub中引入了一个概念叫channel(频道):

  ·发布者通过 PUBLISH 投递消息给指定 channel。

  ·订阅者通过 SUBSCRIBE 订阅它关心的 channel。并且可以订阅一个或者多个。

  和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失(客户端断开连接或者 Redis 宕机都会导致消息丢失)、消息堆积(发布者发布消息的时候不管消费者的具体消费能力如何)不好解决。

  Redis 5.0 新增加的一个数据结构Stream可以用来做消息队列,Stream支持:
    发布 / 订阅模式
    按照消费者组进行消费
    消息持久化( RDB 和 AOF)

  Stream在 Redis 发生故障恢复后不能保证消息至少被消费一次。
  
7.如何基于Redis实现分布式锁?
分布式锁:
  对于单机多线程来说,在Java中,我们通常使用ReetrantLock类、synchronized(JVM实现)关键字来控制一个JVM进程内的多个线程对本地共享资源的访问。
  分布式系统下,不同的服务/客户端通常运行在独立的JVM进程上。如果多个JVM进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是分布式锁就诞生了。
 
一个最基本的分布式锁需要满足:
  互斥 :任意一个时刻,锁只能被一个线程持有;
  高可用 :锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。
  可重入:一个节点获取了锁之后,还可以再次获取锁。
  通常情况下,我们一般会选择基于 Redis(有性能要求)或者 ZooKeeper(有可靠性要求)实现分布式锁,Redis 用的要更多一点。


基于Redis实现分布式锁:
  不论是本地锁还是分布式锁,核心都在于“互斥”。
  在 Redis中,SETNX命令可以帮助我们实现互斥。SETNX即SET if Not exists (对应Java中的setIfAbsent方法),如果key不存在的话,才会设置key的值。如果key已经存在,SETNX 啥也不做。
释放锁的话,直接通过DEL命令删除对应的 key 即可。
  为了防止误删其他锁,这里建议使用Lua脚本通过key对应的value(唯一值)来判断。
  选用Lua脚本是为了保证解锁操作的原子性。因为 Redis 在执行Lua脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

> SETNX lockKey uniqueValue

(integer) 1

> SETNX lockKey uniqueValue

(integer) 0

> DEL lockKey

(integer) 1

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放

if redis.call("get",KEYS[1]) == ARGV[1]

then

  return redis.call("del",KEYS[1])

else

  return 0

end

 
为什么要给锁设置一个过期时间?
  为了避免锁无法被释放,可以给这个 key(也就是锁) 设置一个过期时间 。
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
·  lockKey :加锁的锁名;
·  uniqueValue :能够唯一标示锁的随机字符串;
·  NX :只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
·  EX :过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。
  一定要保证设置指定key的值和过期时间是一个原子操作!!!不然的话,依然可能会出现锁无法被释放的问题。
  这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。
如何实现锁的优雅续期?
  如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!Redisson!
  Redisson是一个开源的Java语言Redis客户端,提供了很多开箱即用的功能,不仅包括多种分布式锁的实现,并且支持Redis单机、Redis Sentinel、Redis Cluster等多种部署架构。
  Redisson中的分布式锁自带自动续期机制,使用起来非常简单,其提供了一个专门用来监控和续期锁的Watch Dog(看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
 
  默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。
  Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期.
  renewExpirationAsync 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。
  这里以Redisson的分布式可重入锁 RLock为例来说明如何使用 Redisson 实现分布式锁:

//1.获取指定的分布式锁对象 RLock lock = redisson.getLock("lock");

// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制 lock.lock();

// 3.执行业务 ...

// 4.释放锁 lock.unlock();

  只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制lock.lock(10, TimeUnit.SECONDS);

 
如何实现可重入锁:
  可重入锁:指的是在一个线程中可以多次获取同一把锁。
  Java 中的 synchronized 和 ReentrantLock 都属于可重入锁。
  不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。
  可重入分布式锁的实现核心思路:线程在获取锁的时候判断是否为自己的锁如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于0时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。
  推荐使用我们上面提到的 Redisson ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。
 
Redis如何解决集群情况下分布式锁的可靠性:
  为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。
  由于Redis集群数据同步到各个节点时是异步的,如果在Redis主节点获取到锁后,在没有同步到其他节点时,Redis主节点宕机了,此时新的Redis主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。导致新的进程有可能拿到锁,但之前的进程以为自己还有锁,那么就出现两个进程拿到了同一个锁的问题。
Redis之父antirez设计了Redlock算法来解决:
  Redlock算法思想让客户端向Redis集群中的多个独立的Redis实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
即使部分Redis节点出现问题,只要保证Redis集群中有半数以上的Redis节点可用,分布式锁服务就是正常的。
  Redlock是直接操作Redis节点的,并不是通过Redis集群操作的,这样才可以避免Redis 集群主从切换导致的锁丢失问题。
  假设目前有 N 个独立的 Redis 实例, 客户端先按顺序依次向 N 个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,但是需要注意的是,Redlock 算法设置了加锁的超时时间,为了避免因为某个 Redis 实例发生故障而一直等待的情况。当客户端完成了和所有 Redis 实例的加锁操作之后,如果有超过半数的 Redis 实例成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。
  Redis主从集群切换数据丢失问题:异步复制同步丢失(如果此时master还没来得及同步给slave节点时发生宕机,那么master内存中的数据会丢失)、集群产生脑裂数据丢失(在发现问题之后,旧的master降为slave同步新的master数据,那么之前的数据被刷新掉,大量数据丢失)

基于Zookeeper实现分布式锁
  ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
  ZooKeeper分布式锁是基于临时顺序节点Watcher(事件监听器)实现的。
  获取锁:
  ·首先我们要有一个持久节点/locks,客户端获取锁就是在locks下创建临时顺序节点。
  · 假设客户端1创建了/locks/lock1节点,创建成功之后,会判断 lock1是否是 /locks 下最小的子节点
  ·如果 lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
  ·如果获取锁失败,则说明有其他客户端已经成功获取锁。客户端1并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端1就加锁成功了。
  释放锁:
  ·成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
  ·成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
  ·我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
 
  实际项目中,推荐使用Curator来实现 ZooKeeper 分布式锁。Curator是Netflix公司开源的一套ZooKeeper Java客户端框架,相比于ZooKeeper自带的客户端zookeeper来说,Curator的封装更加完善,各种API都可以比较方便地使用。
Curator主要实现了下面四种锁
  InterProcessMutex:分布式可重入排它锁
  InterProcessSemaphoreMutex:分布式不可重入排它锁
  InterProcessReadWriteLock:分布式读写锁
  InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
为什么要用临时顺序节点
  每个数据节点在 ZooKeeper 中被称为 znode,它是ZooKeeper中数据的最小单元
我们通常是将 znode 分为 4 大类:
  持久(PERSISTENT)节点:一旦创建就一直存在,即使ZooKeeper集群宕机,直到将其删除。
  临时(EPHEMERAL)节点:临时节点的生命周期是与客户端会话(session)绑定的,会话消失则节点消失。并且临时节点只能做叶子节点,不能创建子节点。
  持久顺序(PERSISTENT_SEQUENTIAL)节点 :除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如/node1/app0000000001、/node1/app0000000002。
  临时顺序(EPHEMERAL_SEQUENTIAL)节点 :除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
 
  临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。
  使用Redis实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而ZooKeeper直接利用临时节点的特性即可。
  假设不适用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁(惊群现象),这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。
 
为什么要设置对前一个节点的监听?
  Watcher(事件监听器),是ZooKeeper中的一个很重要的特性。ZooKeeper允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。
  同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。
  事件监听器的作用:当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java中的 wait/notifyAll),让它尝试去获取锁,然后就成功获取锁了。
 
如何实现可重入锁?
  这里以Curator的 InterProcessMutex 对可重入锁的实现来介绍。
  当我们调用InterProcessMutex#acquire方法获取锁的时候,会调用InterProcessMutex#internalLock方法。
  lockData包含锁的信息和加锁的次数,是实现可重入锁的关键。第一次获取锁的时候, lockData为null。获取锁成功之后,会将当前线程和对应的lockData放到threadData中。
如果已经获取过一次锁,后面再来获取锁的话,直接就会在if (lockData != null) 这里被拦下了,然后就会执行lockData.lockCount.incrementAndGet();将加锁次数加 1。
  整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加1就可以了。

标签:加锁,Redis,基础,获取,分布式,节点,客户端
From: https://www.cnblogs.com/cjhtxdy/p/17691223.html

相关文章

  • S0002-HomeBrew基础入门
    零、homebrewHomebrew是一个自由开源的软件包管理系统,主要设计给AppleMac电脑的操作系统macOS使用,但也支持Linux系统。它可以快速简洁的安装、卸载以及管理计算机软件包。Homebrew的一个重要特点是其包含了一些Mac预装软件缺失的GNU工具,如:bash,git,wget,curl等......
  • Redis7 安装配置
    一、概述由于企业里面做Redis开发,99%的都是Linux版的运用和安装,几乎不会涉及到Windows版二、依赖Linux安装Redis必须先具备gcc编译环境gcc-v安装redis之前需要具备c++库环境yuminstall-ygcc-c++三、版本选择四、安装步骤1、下载命令:wgethttps://download.redis.io/release......
  • Java基础学习——字符串
    目录1String概述 2String构造方法代码实现和内存分析2.1创建方式2.2内存区1.StringTable(串池)2.直接赋值创建字符串方式内存图3.通过new创建字符串方式内存图 3字符串比较3.1“==”号比较的内容    1String概述总结:1.String是Java定义好......
  • MySQL基础
    要学习数据库首先要先搞清楚三个概念数据库(DB):是存储数据的仓库数据库管理系统(DBMS):管理数据库的大型软件SQL:通过SQL操作数据库管理系统操作数据库,对数据库进行增删改查等由此我们可以知道数据库就是安装在操作系统之上的数据仓库,用于存储数据。我们也先认识一个概念->关系型数据......
  • Java基础知识面试题系列五:41~50题
    Java基础知识面试题系列三:41~50题41.值传递与引用传递有哪些区别42.不同数据类型的转换有哪些规则43.强制类型转换的注意事项有哪些44.Math类中round、ceil和floor方法的功能是什么45.++i与i++有什么区别46."<<"运算符与">>"运算符有什么异同47.char型变量中是否可以存储一个中文汉......
  • Java基础知识面试题系列三:21~30题
    Java基础知识面试题系列三:21~30题21.抽象类(abstractclass)与接口(interface)有什么异同22.内部类有哪些23.如何获取父类的类名24.this与super有什么区别25.break、continue以及return有什么区别26.final、finally和finalize有什么区别27.JDK中哪些类是不能继承的28.assert有什么......
  • Java基础知识面试题系列四:31~40题
    Java基础知识面试题系列三:31~40题31.static与final结合使用表示什么意思32.使用switch时有哪些注意事项33.volatile有什么作用34.instanceof有什么作用35.strictfp有什么作用36.Java提供了哪些基本数据类型37.在Java语言中null值时什么?在内存中null是什么38.如何理解赋值语句String......
  • Java基础知识面试题系列七:61~70题
    Java基础知识面试题系列七:61~70题61、JavaIO流的实现机制是什么62、管理文件和目录的类是什么63、如何列出某个目录下的所有目录和文件64、JavaSocket是什么65.用Socket实现客户端和服务器端的通信,要求客户发送数据后能够回显相同的数据66.JavaNIO是什么67.什么是Java序列化68.......
  • Java基础知识面试题系列六:51~60题
    Java基础知识面试题系列六:51~60题51."=="、equals和hashCode有什么区别52.String、StringBuffer、StringBuilder和StringTokenizer有什么区别53.Java中数组是不是对象54.数组的初始化方式有哪几种55.length属性与length()方法有什么区别56.异常处理的原理是什么57.运行时异常和普通......
  • Java基础知识面试题系列八:81~90题
    Java基础知识面试题系列七:81~90题81.JavaCollections框架是什么82.什么是迭代器83.Iterator与ListIterator有什么区别84.ArrayList、Vector和LinkedList有什么区别85.ArrayList、Vector和LinkedList容器使用场景选择86.HashMap、Hashtable、TreeMap和WeakHashMap有哪些区别87.Hash......