Redis学习笔记

一. Redis简介

  • Redis是Nosql数据库之一,是一个开源的、使用C语言编写的、可基于内存也能持久化存储Key-Value数据库。
  • Redis具备以下特性:
  1. 运行在内存,减少IO操作,性能高效
  2. 支持多种数据类型(也就是Value的类型)
  • Redis的应用场景:
    在这里插入图片描述
  1. 分布式架构场景,做session共享;ps:session共享的一般解决方案:1. session(例如用户信息)存储到客户端cookie中,每次请求带着用户信息。2. session复制,将一个服务器中session信息复制到其他服务器,会造成空间浪费,数据冗余;3. 将session信息存储在redis中。
  2. 对于高频次的热门访问数据,做缓存,降低对数据库IO

Nosql数据库简介

  • NoSQL:泛指非关系型的数据库,大数据量,高性能,NoSQL数据库都具有非常高的读写性能,尤其在大数据量下,同样表现优秀。这得益于它的无关系性,数据库的结构简单。
  • Nosql数据库具备以下特性:
  1. 不支持ACID
  2. 远超于SQL的性能
  • Nosql数据库适用场景:海量数据的高并发读写。
  • Nosql数据库有多种类型:
  1. 键值存储数据库:Redis;2. 列式存储数据库:HBase;3. 文档型数据库:MongoDB;4. 图形数据库:InfoGrid

Redis为什么快

面试官:Redis 为什么这么快?除了基于内存操作还有其他原因吗?
Redis是单线程+多路IO复用技术来操作的;
1、单线程避免了线程切换和锁竞争的开销时间,不会发生死锁,所以会快很多。
2、多路IO复用程序同时监听多个Socket,底层基于epoll I/O模型,当有消息到达则会提交到给事件处理器处理,即Reactor模式(时间驱动)不会在 还没到达消息的sokect上浪费时间(不轮询);同时,由于epoll I/O模型也是非阻塞的,Redis线程不会像基本IO模型中一直在阻塞点等待,会处理其他实际到达的链接请求数据
3、高效的数据结构,例如BitMap;

Redis单线程指的是网络请求模块使用了一个线程,即一个线程处理所有网络请求,其他模块可以是多线程。
因为是单线程,瓶颈不是CPU,最有可能是机器内存或者网络宽带

Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。
Redis 6.0中的多线程,也只是针对处理网络请求过程采用了多线程,而数据的读写命令,仍然是单线程处理的。

Redis中的单线程模式

Redis基于Reactor(反应器设计模式)开发了网络事件处理器,称之为文件时间处理器。具体如下图。
在这里插入图片描述
IO多路复用程序会同时监听多个socket,当被监听的socket准备好执行accept、read、write、close等操作时,与这些操作相对应的文件事件就会产生。IO多路复用程序会把所有产生事件的socket压入一个队列中,然后有序地每次仅一个socket的方式传送给文件事件分派器,文件事件分派器接收到socket之后会根据socket产生的事件类型调用对应的事件处理器进行处理。

文件事件处理器分为几种:

  1. 连接应答处理器:用于处理客户端的连接请求;
  2. 命令请求处理器:用于执行客户端传递过来的命令,比如常见的set、lpush等;
  3. 命令回复处理器:用于返回客户端命令的执行结果,比如set、get等命令的结果;

事件种类:

  • AE_READABLE:与两个事件处理器结合使用。
    • 当客户端连接服务器端时,服务器端会将连接应答处理器与socket的AE_READABLE事件关联起来;
    • 当客户端向服务端发送命令的时候,服务器端将命令请求处理器与AE_READABLE事件关联起来;
  • AE_WRITABLE:当服务端有数据需要回传给客户端时,服务端将命令回复处理器与socket的AE_WRITABLE事件关联起来。
    Redis的客户端与服务端的交互过程如下所示
    在这里插入图片描述
Redis中的非阻塞IO

基本IO模型与阻塞点
以Get请求为例,为了处理一个Get请求:

  1. 须要监听客户端请求(bind/listen)
  2. 和客户端创建链接(accept)
  3. 从socket中读取请求(recv)
  4. 解析客户端发送请求(parse)
  5. 根据请求类型读取键值数据(get)
  6. 最后给客户端返回结果,即向socket中写回数据(send)。
    下图显示了这一过程,其中,bind/listen、accept、recv、parse和send属于网络IO处理,而get属性键值数据操做。在这里插入图片描述
    可是在这里的网络IO操做中,有潜在的阻塞点,分别是accept()和recv()。
  • 当Redis监听listen()方法后,客户端的请求一直没有到达,会阻塞在accept()
  • 当Redis调用accept()方法建立连接后,客户端的数据一直没有到达,Redis也会一直阻塞在recv()
    这就致使Redis整个线程阻塞,没法处理其余客户端请求,效率很低。不过,幸运的是,socket网络模型自己支持非阻塞模式。

非阻塞模式主要体现在三个步骤上:

  1. socket()方法会返回主动套接字
  2. 调用listen()方法,可以将主动套接字转换成监听套接字
  3. 调用accept()方法接受客户端的连接 并且返回已连接套接字。

针对上述条件的可以优化点:

  1. listen()方法设置成非阻塞模式,当redis调用accept()也就是接受客户端的连接的时候一直没有请求过来的时候,redis的线程可以返回处理其他事件
  2. accept()方法设置成非阻塞模式,当redis调用recv()接受客户端的数据传输,如果已连接套接字一直没有数据过来,redis线程同样可以处理其他操作。
    在这里插入图片描述
    这样能保证Redis线程既不会像基本IO模型中一直在阻塞点等待,会处理其他实际到达的链接请求数据

Redis的io模型主要是基于epoll实现的,不过它也提供了 selectkqueue的实现,默认采用epoll。
epoll是一种多路复用技术,特点:没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数目和系统内存关系很大

二. Redis支持的数据类型

2.1 String类型

  • Sting底层用的是一个简单动态字符串(Simple Dynamic String,缩写SDS)实现;
  • 二进制安全,可以包含任何数据,比如jpg图片或者序列化的对象(二进制安全:不会对特殊字符进行特殊解释,例如‘\0’解释成字符串的结尾。只严格按照二进制的数据存取,不会妄图以某种特殊格式解析数据)
  • 最大长度512MB
  • 扩容机制:未超过1MB,长度扩容成原来的2倍
    超过1MB,长度扩容1MB。
  • 常用命令:
  1. setnx 只有在 key 不存在时 设置 key 的值
  2. setex <过期时间>,设置键值的同时,设置过期时间,单位秒。
    原子操作:原子操作是指不会被线程调度机制打断的操作;
  3. incr 将 key 中储存的数字值增1,只能对数字值操作,如果为空,新增值为1
  4. decr 将 key 中储存的数字值减1,只能对数字值操作,如果为空,新增值为-1
  5. incrby / decrby <步长>将 key 中储存的数字值增减。自定义步长。

2.2 列表(List)

  • 存储单键多值的数据
  • 底层是双向链表,头尾相连,对两端的操作性能很高
    当列表元素较少时,使用一块连续的内存存储,存储结构时ziplist(压缩列表)
    当列表元素较多时,改成quiplist存储,也就是将多个ziplist使用双向指针串起来组成
  • 常用命令:
    lpush:相当于头插法;rpush:相当于尾插法
    lrange按照索引下标获得元素(从左到右)

2.3 集合(Set)

  • 存储单键无重复多值的数据
  • String类型的无序集合
  • 底层是一个value为null的hash表,所以添加,删除和查找的复杂度都是O(1)
  • 常用命令:
    sadd :将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略。
    sinter 返回两个集合的交集元素。
    sunion 返回两个集合的并集元素。

2.4 有序集合(Zset,sorted set)

  • 存储单键无重复有序多值集合
  • 有序集合的每个成员都关联一个评分,按照评分高低排序集合中的成员
  • 常用命令:
    zadd … 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
    zrank 返回该值在集合中的排名,从0开始。
    案例:如何利用zset实现一个文章访问量的排行榜?
  • 底层数据结构是跳跃表,跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。
    跳跃表和有序有序链表的对比:
    (1)有序链表
    在这里插入图片描述
    要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

(2)跳跃表
在这里插入图片描述
从第2层开始,1节点比51节点小,向后比较。
21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层
在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下
在第0层,51节点为要查找的节点,节点被找到,共查找4次。

从此可以看出跳跃表比有序链表效率要高

2.5 哈希(Hash)

  • hash是一个String类型的field和value(对象)的映射表
    类似Java里面的Map<String,Object>
    在这里插入图片描述

  • 底层结构:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

  • 常用命令:
    hset 给集合中的 键赋值
    hget 从集合取出 value
    hmset … 批量设置hash的值

四. Redis的发布和订阅

  • 发布(指令:publish channel message)和订阅(subscribe channel)是一种消息通信模式。
  • 发布者通过频道发送消息
  • 订阅者(客户端)接收频道的消息,客户端可以订阅任意数量的频道。
  • 发布者和订阅者都是客户端

五. Redis6 新数据类型

Bitmaps(位图)

简介

  • 本质上是字符串类型,可以对字符串的位进行操作(每个字符串由多个字节组成,一个字节占8位,存储上限也是512mb)
  • 可看作是以位为单位,只存储0或1的数组,数组的下标叫做偏移量

命令

1、setbit
(1)格式
setbit设置Bitmaps中某个偏移量的值(0或1)
在这里插入图片描述
*offset:偏移量从0开始

(2)实例
每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。
设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图。
在这里插入图片描述
unique:users:20201106代表2020-11-06这天的独立访问用户的Bitmaps
在这里插入图片描述
注:
很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。、

2、getbit
(1)格式
getbit获取Bitmaps中某个偏移量的值 (时间复杂度是O(1))
在这里插入图片描述
获取键的第offset位的值(从0开始算)
在这里插入图片描述
注:因为100根本不存在,所以也是返回0

3、bitcount
统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。

计算2022-11-06这天的独立访问用户数量
在这里插入图片描述
start和end代表起始和结束字节数, 下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数, 对应的用户id是11, 15, 19。
在这里插入图片描述
注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置。

4、bitop
(1)格式
bitop and(or/not/xor) [key…]
(2)实例
2020-11-04 日访问网站的userid=1,2,5,9。
setbit unique:users:20201104 1 1
setbit unique:users:20201104 2 1
setbit unique:users:20201104 5 1
setbit unique:users:20201104 9 1

2020-11-03 日访问网站的userid=0,1,4,9。
setbit unique:users:20201103 0 1
setbit unique:users:20201103 1 1
setbit unique:users:20201103 4 1
setbit unique:users:20201103 9 1

计算出两天都访问过网站的用户数量
bitop and unique:users:and:20201104_03 unique:users:20201103unique:users:20201104
在这里插入图片描述

应用场景

  1. 统计用户在线状态(偏移量是用户ID)
  2. 统计活跃用户(偏移量是用户ID)
  3. 统计用户签到(键是用户ID,偏移量是天,最大365)

以统计活跃用户为例,说明bitmap的优势
假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表。
帮助理解下面的表Set可以看做是纵向扩展地存储用户数据,而Bitmaps可以看做是横向扩展地存储用户数据,需确定最大长度为1亿,因为bitmap可看作是个数组
在这里插入图片描述
很明显, 这种情况下使用Bitmaps能节省很多的内存空间 ,尤其是随着时间推移节省的内存还是非常可观的。
缺点:假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。
在这里插入图片描述

HyperLogLog

背景

在统计相关的功能需求中,比如统计网站的页面访问量,可以用Redis的incur、incrby轻松实现。
但像UV(独立访客)、独立IP数、搜索记录数等需要去重的统计数如何解决?这种求集合中不重复个数的问题是基数问题。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5

解决基数统计的问题有以下方案:

  • 数据存储在MySQL表中,使用distinct count计算不重复个数
  • 使用Redis提供的hash、set、bitmaps等数据结构来处理
    缺点:随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的

简介

HyperLogLog做基数统计,每个HyperLogLog只需要花费12kb内存,能够计算接近 2^64 个不同元素的基数。
HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身。因此,这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。简单来说,以前的方法是先存储后计算 ,HyperLogLog是不存储直接计算

命令

1、pfadd
(1)格式
pfadd < element> [element …] 添加指定元素到 HyperLogLog 中在这里插入图片描述
(2)实例
在这里插入图片描述
将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0。

2、pfcount
(1)格式
pfcount [key …] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
在这里插入图片描述
(2)实例
在这里插入图片描述
3、pfmerge
(1)格式
pfmerge [sourcekey …] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
在这里插入图片描述
(2)实例
在这里插入图片描述

Geospatial

简介

该类型是二维坐标,存储地理位置信息。Redis基于此类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

命令

1、geoadd
(1)格式
geoadd< longitude> [longitude latitude member…] 添加地理位置(经度,纬度,名称)
在这里插入图片描述
(2)实例
geoadd china:city 121.47 31.23 shanghai
geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.90 beijing
在这里插入图片描述

2、geopos
(1)格式
geopos [member…] 获得指定地区的坐标值
在这里插入图片描述
(2)实例
在这里插入图片描述
3、geodist

(1)格式
geodist [m|km|ft|mi ] 获取两个位置之间的直线距离
在这里插入图片描述
(2)实例
获取两个位置之间的直线距离
在这里插入图片描述
单位:
m 表示单位为米[默认值]。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位

4、georadius
(1)格式
georadius< longitude>radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素
在这里插入图片描述
经度 纬度 距离 单位

(2)实例
在这里插入图片描述

五. Redis 事务_锁机制

事务

定义

  • 本质是一组命令的集合,可以一次执行多个命令,所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许插队
  • Redis事务的主要作用就是串联多个命令防止别的命令插队
  • 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis事务与Mysql事务的区别

  1. Redis事务不保证原子性,redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行
  2. Redis事务不能rollback
  3. Redis事务没有隔离级别的概念

Redis事务执行的三个阶段 (Multi、Exec、discard)

在这里插入图片描述

  • 开启:以MULTI开始一个事务;

  • 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面;(组队的过程中可以通过discard来放弃组队)

  • 执行:由EXEC命令触发事务;

事务的错误处理

组队阶段某个命令出现了报告错误,执行时整个的所有队列都会被取消。
在这里插入图片描述
执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
在这里插入图片描述

事务解决数据冲突的问题

为什么单线程的redis会有数据冲突的问题
  • redis单线程执行指的是redis本身在CURD操作数据的时候时候cmd是 one by one 执行的(命令入队执行),不存在多条命令并发执行的情况,即redis的数据读写模块是单线程的,不会出现多个数据读写模块分别处理一部分命令集合,所有的命令请求串行化执行。
    即时是串行化执行也会导致数据冲突,比如get和set这一对。例如:
    在这里插入图片描述
    预期结果是3,但是实际执行结果是2。解决方法是 事务+乐观锁 来解决。这里要注意,事务和乐观锁是绑定在一起使用的

  • 而redis是支持多客户端连接的,自然会涉及到线程池问题,再高并发的情况下,客户端之间会存在资源竞争。当多个客户端并发操作同一Key值时,就会产生类似于多线程操作的现象。

ps:什么是多线程? 多个“事件处理器”并发处理一个或者多个“事件”,以此论证redis数据读写模块为什么是单线程的
因为redis数据读写模块没有多个事件处理器(数据读写模块)。而多个客户端可以操作同一数据,因此是多线程的,客户端可看作是事件处理器。

乐观锁与悲观锁

  • 悲观锁:很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。在Redis中,乐观锁的作用就是确保 事务开启执行时操作的数据 与 前面拿到的数据(做了一些判断)的值是一样的
乐观锁的具体实现

WATCH key [key …]
在执行multi之前,先执行watch key1[key2],可以监视一个或者多个key,如果在事务执行之前这个key被其他命令所改动,那么事务将被打断。简单来说,在执行multi命令之前,也就是命令入队列之前读取数据的版本号,在执行exec命令之前检查并更新版本号。
在这里插入图片描述
unwatch key [key …]
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

六. Redis 事务_秒杀案例

操作两个Key(商品id+商品对应的秒杀成功用户id)

在这里插入图片描述

Redis事务–秒杀并发模拟

使用工具ab(apache bench,web性能测试工具)模拟测试
下载:yum install httpd-tools
测试命令:ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.2.115:8081/Seckill/doseckill

Usage: ab [options] [http[s]://]hostname[:port]/path
用法:ab [选项] 地址

选项:
Options are:
    -n requests    #执行的请求数,即一共发起多少请求。
    -c concurrency    #请求并发数。
    -t timelimit    #测试所进行的最大秒数。其内部隐含值是-n 50000,它可以使对服务器的测试限制在一个固定的总时间以内。默认时,没有时间限制。
    -s timeout    #指定每个请求的超时时间,默认是30秒。
    -b windowsize    #指定tcp窗口的大小,单位是字节。
    -B address    #指定在发起连接时绑定的ip地址是什么。
    -p postfile    #指定要POST的文件,同时要设置-T参数。
    -u putfile    #指定要PUT的文件,同时要设置-T参数。
    -T content-type    #指定使用POSTPUT上传文本时的文本类型,默认是'text/plain'-v verbosity    #设置详细模式等级。
    -w    #将结果输出到html的表中。
    -i    #使用HEAD方式代替GET发起请求。
    -y attributes    #以表格方式输出时,设置html表格tr属性。 
    -z attributes    #以表格方式输出时,设置html表格th或td属性。
    -C attribute    #添加cookie,比如'Apache=1234'。(可重复)
    -H attribute    #为请求追加一个额外的头部,比如'Accept-Encoding: gzip'。(可重复)
    -A attribute    #对服务器提供BASIC认证信任。用户名和密码由一个:隔开,并以base64编码形式发送。无论服务器是否需要(,是否发送了401认证需求代码),此字符串都会被发送。
    -P attribute    #对一个中转代理提供BASIC认证信任。用户名和密码由一个:隔开,并以base64编码形式发送。无论服务器是否需要(, 是否发送了401认证需求代码),此字符串都会被发送。
    -X proxy:port   #指定代理服务器的IP和端口。
    -V              #打印版本信息。
    -k              #启用HTTP KeepAlive功能,即在一个HTTP会话中执行多个请求。默认时,不启用KeepAlive功能。
    -d              #不显示"percentage served within XX [ms] table"的消息(为以前的版本提供支持)-q              #如果处理的请求数大于150,ab每处理大约10%或者100个请求时,会在stderr输出一个进度计数。此-q标记可以抑制这些信息。
    -g filename     #把所有测试结果写入一个'gnuplot'或者TSV(以Tab分隔的)文件。此文件可以方便地导入到Gnuplot,IDL,Mathematica,Igor甚至Excel中。其中的第一行为标题。
    -e filename     #产生一个以逗号分隔的(CSV)文件,其中包含了处理每个相应百分比的请求所需要(1%100%)的相应百分比的(以微妙为单位)时间。由于这种格式已经“二进制化”,所以比'gnuplot'格式更有用。
    -r              #当收到错误时不要退出。
    -h              #输出帮助信息
    -Z ciphersuite  指定SSL/TLS密码套件
    -f protocol     指定SSL/TLS协议(SSL3, TLS1, TLS1.1, TLS1.2 or ALL)

连接池

1.基本原理:在内部对象池中,维护一定数量的数据库连接,并对外暴露数据库连接的获取释放方法。

如外部使用者可通过getConnection方法获取数据库连接,使用完毕后再通过releaseConnection方法将连接返回,注意此时的连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。

2.好处

①资源重用 (连接复用)

  由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,增进了系统环境的平稳性(减少内存碎片以级数据库临时进程、线程的数量)

②更快的系统响应速度

  数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池内备用。此时连接池的初始化操作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。

③新的资源分配手段

  对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接技术。

④统一的连接管理,避免数据库连接泄露

 在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用的连接,从而避免了常规数据库连接操作中可能出现的资源泄露

使用Jedis线程池不需要创建新的Jedis对象连接Redis,不是随用随创,可以大大减少对于创建和回收Redis连接的开销,(建立tcp/ip连接,具体过程参考三次握手)

public class JedisPoolUtil {
	private static volatile JedisPool jedisPool = null;

	private JedisPoolUtil() {
	}

	public static JedisPool getJedisPoolInstance() {
		if (null == jedisPool) {
			synchronized (JedisPoolUtil.class) {
				if (null == jedisPool) {
					JedisPoolConfig poolConfig = new JedisPoolConfig();
					poolConfig.setMaxTotal(200);
					poolConfig.setMaxIdle(32);
					poolConfig.setMaxWaitMillis(100*1000);
					poolConfig.setBlockWhenExhausted(true);
					poolConfig.setTestOnBorrow(true);  // ping  PONG
				 
					jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
				}
			}
		}
		return jedisPool;
	}

	public static void release(JedisPool jedisPool, Jedis jedis) {
		if (null != jedis) {
			jedisPool.returnResource(jedis);
		}
	}

}

利用事务+乐观锁解决超卖问题

public static boolean doSecKill(String uid,String prodid) throws IOException {
		//1 uid和prodid非空判断
		if(uid == null || prodid == null) {
			return false;
		}

		//2 连接redis
		//Jedis jedis = new Jedis("192.168.44.168",6379);
		//通过连接池得到jedis对象
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis = jedisPoolInstance.getResource();

		//3 拼接key
		// 3.1 库存key
		String kcKey = "sk:"+prodid+":qt";
		// 3.2 秒杀成功用户key
		String userKey = "sk:"+prodid+":user";

		//监视库存
		jedis.watch(kcKey);

		//4 获取库存,如果库存null,秒杀还没有开始
		String kc = jedis.get(kcKey);
		if(kc == null) {
			System.out.println("秒杀还没有开始,请等待");
			jedis.close();
			return false;
		}

		// 5 判断用户是否重复秒杀操作
		if(jedis.sismember(userKey, uid)) {
			System.out.println("已经秒杀成功了,不能重复秒杀");
			jedis.close();
			return false;
		}

		//6 判断如果商品数量,库存数量小于1,秒杀结束
		if(Integer.parseInt(kc)<=0) {
			System.out.println("秒杀已经结束了");
			jedis.close();
			return false;
		}

		//7 秒杀过程
		//使用事务
		Transaction multi = jedis.multi();

		//组队操作
		multi.decr(kcKey);
		multi.sadd(userKey,uid);

		//执行
		List<Object> results = multi.exec();

		if(results == null || results.size()==0) {
			System.out.println("秒杀失败了....");
			jedis.close();
			return false;
		}

		//7.1 库存-1
		//jedis.decr(kcKey);
		//7.2 把秒杀成功用户添加清单里面
		//jedis.sadd(userKey,uid);

		System.out.println("秒杀成功了..");
		jedis.close();
		return true;
	}

利用LUA脚本解决库存遗留问题

  • 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
  • LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
  • 但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
  • 利用lua脚本淘汰用户,解决超卖问题。
  • redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题
public class SecKill_redisByScript {
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

	public static void main(String[] args) {
		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
 
		Jedis jedis=jedispool.getResource();
		System.out.println(jedis.ping());
		
		Set<HostAndPort> set=new HashSet<HostAndPort>();

	//	doSecKill("201","sk:0101");
	}
	
	static String secKillScript ="local userid=KEYS[1];\r\n" + 
			"local prodid=KEYS[2];\r\n" + 
			"local qtkey='sk:'..prodid..\":qt\";\r\n" + 
			"local usersKey='sk:'..prodid..\":usr\";\r\n" + 
			"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + 
			"if tonumber(userExists)==1 then \r\n" + 
			"   return 2;\r\n" + 
			"end\r\n" + 
			"local num= redis.call(\"get\" ,qtkey);\r\n" + 
			"if tonumber(num)<=0 then \r\n" + 
			"   return 0;\r\n" + 
			"else \r\n" + 
			"   redis.call(\"decr\",qtkey);\r\n" + 
			"   redis.call(\"sadd\",usersKey,userid);\r\n" + 
			"end\r\n" + 
			"return 1" ;
			 
	static String secKillScript2 = 
			"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
			" return 1";

	public static boolean doSecKill(String uid,String prodid) throws IOException {

		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis=jedispool.getResource();

		 //String sha1=  .secKillScript;
		String sha1=  jedis.scriptLoad(secKillScript);
		Object result= jedis.evalsha(sha1, 2, uid,prodid);

		  String reString=String.valueOf(result);
		if ("0".equals( reString )  ) {
			System.err.println("已抢空!!");
		}else if("1".equals( reString )  )  {
			System.out.println("抢购成功!!!!");
		}else if("2".equals( reString )  )  {
			System.err.println("该用户已抢过!!");
		}else{
			System.err.println("抢购异常!!");
		}
		jedis.close();
		return true;
	}
}

七. Redis持久化之RDB

简介

在指定的时间间隔内将内存中数据集的快照写入磁盘,恢复时将快照文件直接读到内存里。

持久化具体是怎么执行的

既然RDB机制是通过把某个时刻的所有数据生成一个快照来保存,那么就应该有一种触发机制,是实现这个过程。对于RDB来说,提供了三种机制:save、bgsave、自动化。我们分别来看一下

  1. save触发方式:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。具体流程如下:
    在这里插入图片描述
  2. bgsave触发方式:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体流程如下:
    在这里插入图片描述
    具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令
    在这里插入图片描述
  • 具体执行过程是:Redis进程短暂阻塞,这时fork一个子进程,子进程负责利用写时复制技术先将数据写入到一个临时文件中,待持久化过程结束后,将临时文件替换上次持久化好的文件(dump.rdb文件)。而主进程不参与任何IO操作,响应客户端的请求
  • 写时复制技术:父进程fork子进程不会直接给子进程复制父进程地址空间的一个副本 ,而是与父进程共享地址空间,只有当父进程有写入操作的时候,才会分配父进程的一个地址空间副本,这么做是为了实现快速的进程创建;如果没有写入操作,那么可以省掉这一步骤、
  • 优点
    1.适合大规模的数据恢复
    2.节省磁盘空间
    3.恢复速度快
  • 缺点
    1.Fork的时候,父进程中的数据被克隆了一份,大致2倍内存消耗
    2.Redis发生故障,可能会丢失最后一次持久化的数据

RDB持久化操作命令

配置文件中默认的快照配置

在这里插入图片描述
900秒内Redis数据库有一条数据被修改则触发RDB
300秒内有10条数据被修改则触发RDB
60秒内有10000条数据被修改则触发RDB
注:如果同时使用RDB和AOF,则RDB的该配置仅保留save 900 1即可,RDB用作AOF数据文件的备用。## 八. Redis持久化之AOF(Append Only File)
底层实现:dirty计数器 用来 存储修改数据量; last save time 保存上一次存储时间;

简介

以日志的形式每次在文件的末尾追加记录redis的写操作,只追加文件不改写文件;Redis启动时会读取日志文件转换成Redis写命令,重新执行一遍完成数据恢复。注:如果同时启用RDB(默认开启)和AOF(非默认开启),那么则优先以AOF方式恢复,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.

持久化流程

1、客户端的请求写命令会被append追加到AOF缓冲区内;
2、AOF缓冲区根据AOF同步频率[always(写一个命令持久化到AOF文件中一次)、everysec(隔一秒)、no(不主动同步,同步时机交给OS)将操作同步到磁盘的AOF文件中
3、AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
4、Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的
在这里插入图片描述
Rewrite压缩

  • 何时重写?默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
  • 4.0版本之前的rewrite:将重复的命令去掉,来书保留最新的命令。例如对于set指令来说,保留新的set指令
  • 4.0版本之前的rewrite:支持混合模式(也是就是rdb和aof一起用),直接将rdb持久化的方式来操作将二进制内容覆盖到aof文件中(rdb是二进制,所以很小),然后再有写入的话还是继续append追加到文件原始命令,等下次文件过大的时候再次rewrite(还是按照rdb持久化的方式将内容覆盖到aof中)。

特点

  • 优势:
    1、备份机制更稳健,丢失数据概率更低。
    2、可读的日志文本,通过操作AOF稳健,可以异常恢复。

  • 劣势:
    1、存储的是命令,占用更多的磁盘空间
    2、恢复速度慢,因为需要重新执行一遍指令
    3、同步频率设置成每次写入同步,会降低性能
    4、存在个别bug,恢复的数据和原来的数据不一致

总结

  • 官方推荐两个都启用。
  • 如果对数据的完整性要求不高,可以选单独用RDB。
  • 不建议单独用 AOF,因为可能会出现Bug。
  • 如果只是做纯内存缓存,可以都不用。

九. Redis主从复制

简介

主机数据更新后根据配置和策略,自动同步到备机的 master/slaver机制,Master以写为主,Slaver以读为主。

优势

  • 读写分离,扩展主节点的数据读取能力
  • 容灾快速恢复

劣势

  • 复制延时
    由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

如何应用

新建从服务器的redis文件
  1. 拷贝多个从服务器的redis.conf文件,include主服务器的redis.conf文件路径
    include /myredis/redis.conf
  2. 指定pid文件
    pidfile /var/run/redis_6379.pid
  3. 指定端口
    port 6379
  4. 指定rdb文件
    dbfilename dump6379.rdb
    在这里插入图片描述
启动三台服务器

在这里插入图片描述

查看三台主机运行情况

指令info replication :打印主从复制的相关信息
在这里插入图片描述

配从(库)不配主(库)

slaveof :成为某个实例的从服务器
1、在6380和6381上执行: slaveof 127.0.0.1 6379
主机只写,从机只读在这里插入图片描述

配置完成后宕机情况
  • 宕机的是主机:重启后依然是主机,从机依然是从机
  • 宕机的是从机:重启后从机需要重设 slaveof 主机ID:端口号,恢复数据

复制原理:全量同步和增量同步

在这里插入图片描述
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

  • 从服务器连接主服务器,发送SYNC命令;
  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

增量同步

  • Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
  • 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

主从复制模式

薪火相传(体现在拓扑结构)

树形结构,Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻上层master的写压力,去中心化降低风险。

  • 风险是某个主机宕机,后面的下层从机都没法备份
    在这里插入图片描述
反客为主(体现在主机发生故障后的处理)

当一个master宕机后,从属的一个slave可以立刻升为master,其后面的slave不用做任何修改。
手动反客为主方式:用 slaveof no one 将从机变为主机。
在这里插入图片描述

哨兵模式(Sentinel)

反客为主的自动版,创建一个redis数据库当做哨兵,后台监控主机是否故障,如果故障了根据优先级、偏移量或者runid自动将从库转换为主库。原主机重启后会变为从机。
在这里插入图片描述
应用

  1. 自定义的/myredis目录下新建sentinel.conf文件
  2. 配置哨兵,填写内容:
    sentinel monitor mymaster 127.0.0.1 6379 1
    其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
  3. 启动哨兵:执行redis-sentinel /myredis/sentinel.conf

故障恢复

在这里插入图片描述

  • 优先级在redis.conf中默认:slave-priority 100,值越小优先级越高
  • 偏移量是指获得原主机数据最全的
  • 每个redis实例启动后都会随机生成一个40位的runid

十. Redis集群

简介

Redis集群实现了对Redis的水平扩容,即启动N个Redis服务器,将整个数据库分布式存储在这N个节点中,每个节点存储总数据的1/N

优势

当单服务器的内存、并发、流量成为瓶颈时
1、用集群的形式重点扩展主节点的数据写入能力(主从复制模式重点是扩展主节点的数据读性能)
2、用集群的架构方案同时达到负载均衡的目的
3、集群是无中心化代理的架构,配置比较简单
4、哨兵模式解决主机故障,提高可用性

劣势

1、不支持多键操作
2、不支持多键的Redis事务,不支持lua脚本
3、 集群方案出线比较晚,很多公司已经采取了其他的架构方案,过度到集群方案比较困难

应用实现

1、将rdb、aof文件都删除掉
2、配置6个服务器
include /home/bigdata/redis.conf:include 主服务器的redis.conf文件路径
pidfile “/var/run/redis_6379.pid”:Pid文件名字
port 6379:指定端口
dbfilename “dump6379.rdb” :Dump.rdb名字
cluster-enabled yes:打开集群模式
cluster-config-file nodes-6379.conf :设定节点配置文件名
cluster-node-timeout 15000 :设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。
3、启动6个redis服务
在这里插入图片描述
4、将6个redis服务合成一个集群
在任意一个Redis连接中 cd /opt/redis-6.2.1/src文件
然后运行:redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391
注:此处不要用127.0.0.1, 请用真实IP地址 ;–replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。

5、 -c 采用集群策略连接,设置数据会自动切换到相应的写主机
在这里插入图片描述
6、通过 cluster nodes 命令查看集群信息
在这里插入图片描述
7、ps:redis cluster 如何分配这六个节点?
一个集群至少要有三个主节点。
选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上,以保证容灾能力和高可用性。

slots(插槽)

  • [OK] All 16384 slots covered.
    节点 A 负责处理 0 号至 5460 号插槽。
    节点 B 负责处理 5461 号至 10922 号插槽。
    节点 C 负责处理 10923 号至 16383 号插槽。
  • 集群中的每个节点平均负责处理一部分插槽,插槽中存储的是Key。计算每个key的校验和,并对插槽总数求余来分配这个key属于哪个插槽
  • 如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向——将key送往对应的redis服务器。
  • 不在一个slot下的键值,是不能使用mget,mset等多键操作。
    在这里插入图片描述
    可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。
    在这里插入图片描述

故障恢复

如果主节点下线?从节点能否自动升为主节点?注意:15秒超时
在这里插入图片描述
主节点恢复后,主从关系会如何?主节点回来变成从机。
在这里插入图片描述
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。
redis.conf中的参数 cluster-require-full-coverage

十一. 应用问题解决

缓存穿透

在这里插入图片描述

  • 问题描述:
    缓存穿透,是指大量请求查询一个数据库一定不存在的数据,从缓存获取不到,大量请求查询数据库导致数据库崩溃。例如:黑客攻击
  • 解决方案:
    1、对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)作为value值进行缓存。这样可以防止攻击用户反复用同一个id暴力攻击。
    2、采用布隆过滤器:使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;
    3、实时监控:当发现Redis的命中率开始急速降低时,设置黑名单限制服务。
    4、拦截器校验:接⼝层增加校验,如给id做校验,id<=0的直接拦截。

缓存击穿

在这里插入图片描述

  • 问题描述:
    缓存击穿,是指一个热点Key被大量访问,当这个key在失效过期的瞬间,高并发请求就穿破缓存转向数据库,数据库请求量骤增导致崩溃。
  • 解决方案:
    1、增加热门数据的过期时长:预先设置,或者通过实时监控实时调整。
    2、使用互斥锁在数据库中对改数据加锁,防止大量请求都去数据库重复取数据,重复往缓存中更新数据情况出现。参考代码如下:在访问数据库的阶段,如果获得锁,则获取数据并更新缓存数据;如果没有获得锁,则访问缓存。
    在这里插入图片描述

缓存雪崩

  • 问题描述:
    缓存雪崩,指缓存中大批量数据到达过期时间,失效的瞬间造成访问这些大批量数据的请求转向数据库,数据库请求量骤增导致崩溃。
  • 解决方案:
    1、构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等) 上级缓存失效时间设置为短期,下级缓存的失效时间设置为长期;
    2、将缓存失效时间设置随机:防止同一时间大量数据过期现象发生,热门数据设置过期时间长,冷门数据设置过期时间短一些;
    3、使用互斥锁:防止大量请求都去数据库重复取数据,重复往缓存中更新数据情况出现。缺点是:吞吐量下降。

十二. 分布式锁

问题描述

在分布式系统中,多线程和多进程分布在不同的redis服务器上,需要用分布式多来控制对共享资源的访问。但仅仅靠Java API无法提供分布式锁的能力。目前主流的分布式锁的实现方案:
1、基于数据库的乐观锁实现分布式锁
2、基于缓存(Redis等):性能最高
3、基于Zookeeper:可靠性最高

使用redis实现分布式锁

  • 加锁:通过setnx指令创建key的方式来创建锁,key不存在创建(获取锁)成功,返回1.当key存在时,key创建(获取锁)失败,返回0
  • 解锁:把key当成锁,释放锁即通过del指令删除key;
  • 锁超时:通过expire指令为key设置一个过期时间,以保证锁没有被显式释放时,到达一定时间后会自动释放。
  • 为什么要给锁配一个超时时间?因为如果在占有锁的时候有异常抛出,那么当前锁不会被释放,其他线程也得不到该锁

编写代码

@GetMapping("testLock")
public void testLock(){
    //1获取锁,setnex
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 3, TimeUnit.SECONDS);
    //2获取锁成功、查询num的值
    if(lock){
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判断num为空return
        if(StringUtils.isEmpty(value)){
            return;
        }
        //2.2有值就转成成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
        //2.4释放锁,del
        redisTemplate.delete("lock");

    }else{
        //3获取锁失败、每隔0.1秒再获取
        try {
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

防误删

  • 上述代码容易导致误删,例如线程A获得锁之后,没执行完业务也没有主动释放锁,超时释放了锁。线程B此时拿到了锁执行任务,此时线程A执行完了任务,接着线程A执行删除锁操作,误删了线程B加的锁。

如何避免呢?

  • 加锁时,value设置一个随机数,释放锁时,判断value的值是否是加锁时设置的数。
  • 在主动释放锁之前做一个判断,验证当前的锁是不是自己加的锁。具体实现可以把当前线程ID或者随机生成一个数存储在锁的value中,删除之前验证锁的value是否与设置的value一致。代码如下:

在这里插入图片描述
或者用线程ID:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
    
  // 判断加锁与解锁是不是同一个客户端
  if (requestId.equals(jedis.get(lockKey))) {
    // 若在此时,这把锁突然不是这个客户端的,则会误解锁
    jedis.del(lockKey);
  }

}

保证原子性

  • 上述代码隐含了一个新的问题,判断Value和释放锁是两个独立操作,不具有原子性。假设线程A在判断value操作完成后,主动删除锁之前,锁的时间过期自动释放,线程B拿到了锁执行任务,此时线程A执行删除锁操作,误删了线程B加的锁。

如何避免呢?

  • 使用lua脚本,将判断Value和释放锁是两个独立操作变成原子操作,代码如下:
@GetMapping("testLockLua")
public void testLockLua() {
    //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
    String uuid = UUID.randomUUID().toString();
    //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
    String skuId = "25"; // 访问skuId 为25号的商品 100008348542
    String locKey = "lock:" + skuId; // 锁住的是每个商品的数据

    // 3 获取锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);

    // 第一种: lock 与过期时间中间不写任何的代码。
    // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
    // 如果true
    if (lock) {
        // 执行的业务逻辑开始
        // 获取缓存中的num 数据
        Object value = redisTemplate.opsForValue().get("num");
        // 如果是空直接返回
        if (StringUtils.isEmpty(value)) {
            return;
        }
        // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
        int num = Integer.parseInt(value + "");
        // 使num 每次+1 放入缓存
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        /*使用lua脚本来锁*/
        // 定义lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用redis执行lua执行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 设置一下返回值类型 为Long
        // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
        // 那么返回字符串与0 会有发生错误。
        redisScript.setResultType(Long.class);
        // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
        redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
    } else {
        // 其他线程等待
        try {
            // 睡眠
            Thread.sleep(1000);
            // 睡醒了之后,调用方法。
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

详解:定义key,key应该是为每个sku定义的,也就是每个sku有一把锁。
String locKey =“lock:”+skuId; // 锁住的是每个商品的数据
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid,3,TimeUnit.SECONDS);
在这里插入图片描述
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须具有原子性。

十三. Redis6.0新功能

ACL(访问控制列表)

Redis 6 则提供ACL的功能对用户进行更细粒度的权限控制:
(1)接入权限:用户名和密码
(2)可以执行的命令
(3)可以操作的 KEY

IO多线程

IO多线程其实指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程。Redis 6 加入多线程,但跟 Memcached 这种从 IO处理到数据访问多线程的实现模式有些差异。Redis 的**多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。**之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。
另外,多线程IO默认也是不开启的,需要再配置文件中配置
io-threads-do-reads yes
io-threads 4

支持 Cluster

之前老版Redis想要搭集群需要单独安装ruby环境,Redis 5 将 redis-trib.rb 的功能集成到 redis-cli 。另外官方 redis-benchmark 工具开始支持 cluster 模式了,通过多线程的方式对多个分片进行压测。

Redis6新功能还有:
1、RESP3新的 Redis 通信协议:优化服务端与客户端之间通信
2、Client side caching客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。
3、Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。
4、Modules API
Redis 6中模块API开发进展非常大,因为Redis Labs为了开发复杂的功能,从一开始就用上Redis模块。Redis可以变成一个框架,利用Modules来构建不同系统,而不需要从头开始写然后还要BSD许可。Redis一开始就是一个向编写各种系统开放的平台。


版权声明:本文为lxh_____原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。