Redis 学习整理


前言

千里之行,始于足下


一、NoSQL

NoSQL(Not Only SQL):指的是非关系型数据库,它不同于传统的关系型数据库,不依赖于业务逻辑方式的存储,而是以简单的Key-Value模式存储,它不需要遵循SQL标准,不支持ACID属性,数据之间没有关系,方便扩展;数据类型多样性;基于内存,性能高;支持大数据量读写。

二、Redis简介

redis 是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据库。它支持存储的数据类型多样化,支持数据交集、并集和差集等丰富操作。同时还支持不同方式的排序。数据都是缓存在内存中,因此读写效率非常高。它还可以对数据做一些持久化的操作,防止数据的丢失。

使用场景:排行榜、计数器、秒杀、发布订阅等等

三、Redis下载和安装

1. centos 7环境下安装Redis

① 下载

wget http://download.redis.io/releases/redis-6.2.7.tar.gz

② 解压

tar -zxvf redis-6.2.7.tar.gz

③ redis6.0及以上版本,gcc的版本需要5.0以上,这里把gcc版本更新下

# gcc版本更新
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
scl enable devtoolset-9 bash
echo “source /opt/rh/devtoolset-9/enable” >>/etc/profile
gcc -v # 查看gcc版本

④ 安装

cd redis-6.2.7
make
make install   //确认安装的东西

⑤ 查看并修改一些配置

cd /usr/local/bin  # 默认安装位置
mkdir redisconf  # 创建一个文件夹放redis的配置
cp /app/redis-6.2.7/redis.conf redisconf/ # 复制一份默认配置到新建的文件夹
vim redisconf/redis.conf
# 需要改的参数如下
daemonize yes   #默认no,改为yes意为以守护进程方式启动,可后台运行
# bind 127.0.0.1 #注释掉这部分
protected-mode no #默认yes,开启保护模式,限制为本地访问。
requirepass 123456   # 配置redis密码
# 改完配置后重启redis服务
ps -ef | grep redis
kill -9 进程号
redis-server redisconf/redis.conf

⑥ 如果要远程连接,需要开放防火墙6379端口和云服务器安全组端口

# 添加6379端口
firewall-cmd --zone=public --add-port=6379/tcp --permanent #(–permanent永久生效,没有此参数重启后失效)
# 重新载入
firewall-cmd --reload
# 查看
firewall-cmd --zone=public --query-port=6379/tcp
firewall-cmd --zone=public --list-ports # 查看所有开放的端口
# 删除
firewall-cmd --zone=public --remove-port=6379/tcp --permanent

2. redis 相关知识介绍

  • redis 默认端口是6379
  • redis 默认有16个数据库,从0 - 15,初始默认使用0数据库,可以使用select命令来切换数据库。如:select 2。
  • dbsize 查看当前数据库的key的数量;flushdb 清空当前库;flushall 清空所有库;auth xxx 输入密码

3. redis 单线程 + IO多路复用

redis 底层采用的是单线程 + 多路 IO 复用技术,单线程的好处就是避免线程上下文切换带来的额外开销,效率更高,同时为了解决单线程线性执行导致的阻塞问题,使用了 I/O 多路复用技术。

I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
在这里插入图片描述

四、Redis 常用五大数据类型

1. key 相关命令

1、查看或匹配当前库所有key

keys *  # 看当前库中所有key
key *k # 匹配以k结尾的所有key

2、判断某个key是否存在

exists key

3、查看key是什么类型

type key

4、删除指定的key

del key

5、根据value选择非阻塞的删除

# 将key 从keyspace 元数据中删除,真正的删除会在后续异步操作
unlink key

6、给key设置过期时间

expire key 10

7、查看key的过期时间,-1表示永不过期,-2表示已过期

ttl key

2. String

String 是 redis 中最基本的数据类型,一个key对应一个value,String类型是二进制安全的,意味着它可以包含任何数据(图片、序列化对象等等),String 中value最多可以是 512M。

String 的底层数据结构为简单动态字符串,是可以修改的字符串,内部结构类似java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
当前key的值value实际分配的空间一般要高于实际value的长度,当字符串长度小于1M时,超出空间容量时会发生扩容,扩容是现有空间的2倍,如果超过1M,扩容时一次只会多扩1M的空间,最长到512M。

1、添加数据

# 如果给重复的key设置value,会进行覆盖
set key value

2、根据key获取数据

get key

3、将给定的value追加到原值末尾

append key value

4、获取值的长度

strlen key

5、只有在key不存在的时候,设置key的值,如果已存在则不进行设置

setnx key value

6、将key中存储的value值加一,只能对数字操作,不然报错

incr key

7、将key中存储的value值减一,只能对数字操作

decr key

8、将key中存储的value值增减指定步长

incrby key 10
decrby key 10

9、同时设置多个key-value,根据多个key获取多个value

mset k1 v1 k2 v2 k3 v3
mget k1 k2 k3
# 这里涉及到原子性,其中有一个失败,其它就全部设置失败
msetnx k4 v4 k5 v5 k1 v6

10、获取值的范围,类似于java中的截字符串,前后都包含

getrange key startIndex endIndex

10、用value覆盖key所存储的字符串值,索引从0开始

setrange key startIndex value

11、设置值的同时设置过期时间,单位是秒

setex key 10 value

12、设置新值的同时获得旧值,新值替换旧值

getset key value

练习:

127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> auth 123456
(error) WRONGPASS invalid username-password pair or user is disabled.
127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> keys *ed
(empty array)
127.0.0.1:6379> set name jacklove
OK
127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> set weight 50
OK
127.0.0.1:6379> keys *
1) "weight"
2) "age"
3) "name"
127.0.0.1:6379> keys *e
1) "age"
2) "name"
127.0.0.1:6379> get name
"jacklove"
127.0.0.1:6379> append name you
(integer) 11
127.0.0.1:6379> get name
"jackloveyou"
127.0.0.1:6379> strlen name
(integer) 11
127.0.0.1:6379> setnx name lucy
(integer) 0
127.0.0.1:6379> setnx tall se
(integer) 1
127.0.0.1:6379> incr age
(integer) 19
127.0.0.1:6379> decr age
(integer) 18
127.0.0.1:6379> incr name
(error) ERR value is not an integer or out of range
127.0.0.1:6379> decr name
(error) ERR value is not an integer or out of range
127.0.0.1:6379> incrby age 10
(integer) 28
127.0.0.1:6379> decrby age 10
(integer) 18
127.0.0.1:6379> met k1 v1 k2 v2 k3 v3
(error) ERR unknown command `met`, with args beginning with: `k1`, `v1`, `k2`, `v2`, `k3`, `v3`, 
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k4 v4 k3 v3
(integer) 0
127.0.0.1:6379> msetnx k6 v6 k7 v7
(integer) 1
127.0.0.1:6379> getrange name 0 3
"jack"
127.0.0.1:6379> getrange name 0 90
"jackloveyou"
127.0.0.1:6379> setrange name 3 abc
(integer) 11
127.0.0.1:6379> get name
"jacabcveyou"
127.0.0.1:6379> 
127.0.0.1:6379> setex key1 10 value1
OK
127.0.0.1:6379> ttl key1
(integer) 7
127.0.0.1:6379> ttl key1
(integer) 5
127.0.0.1:6379> getset age 100
"18"

3. List

List 是单key多value的数据结构,它是一个简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边)。
它的底层实际是一个双向链表,对两端的操作性能很高,通过索引下标操作中间节点性能会较差。

常用命令

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
# 从左边插入一个或者多个值
127.0.0.1:6379> lpush name jack amy lucy
(integer) 3
# 查看所有的值 0 到-1是查看所有
127.0.0.1:6379> lrange name 0 -1
1) "lucy"
2) "amy"
3) "jack"
# 从右边插入一个或者多个值
127.0.0.1:6379> rpush car_name BMW BZ BYD
(integer) 3
127.0.0.1:6379> lrange cre_name 0 -1
(empty array)
127.0.0.1:6379> lrange car_name 0 -1
1) "BMW"
2) "BZ"
3) "BYD"
# 从左边吐出一个或者n个值,值在键在,值吐完了键也没了
127.0.0.1:6379> lpop name
"lucy"
127.0.0.1:6379> lpop name 2
1) "amy"
2) "jack"
127.0.0.1:6379> lpop name
(nil)
127.0.0.1:6379> exists name
(integer) 0
# 从右边吐出一个或者n个值,值在键在,值吐完了键也没了
127.0.0.1:6379> rpop car_name
"BYD"
127.0.0.1:6379> rpop car_name
"BZ"
127.0.0.1:6379> rpop car_name
"BMW"
127.0.0.1:6379> exists car_name
(integer) 0
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> lpush k1 v1 v2 v3
(integer) 3
127.0.0.1:6379> lrange k1 0 -1
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> rpush k2 v4 v5 v6
(integer) 3
127.0.0.1:6379> lrange k2 0 -1
1) "v4"
2) "v5"
3) "v6"
# 将k1最右边的一个元素放到k2最左边
127.0.0.1:6379> rpoplpush k1 k2
"v1"
127.0.0.1:6379> lrange k1 0 -1
1) "v3"
2) "v2"
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v4"
3) "v5"
4) "v6"
# 获取一个key中指定索引位置的元素
127.0.0.1:6379> lindex k2 0
"v1"
127.0.0.1:6379> lindex k2 1
"v4"
# 获取列表的长度
127.0.0.1:6379> llen k2
(integer) 4
127.0.0.1:6379> llen k1
(integer) 2
127.0.0.1:6379> llen k
(integer) 0
# 在列表中某个值的前面或者后面插入一个值
127.0.0.1:6379> linsert k2 before v5 v55
(integer) 5
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v4"
3) "v55"
4) "v5"
5) "v6"
127.0.0.1:6379> linsert k2 after v5 v66
(integer) 6
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v4"
3) "v55"
4) "v5"
5) "v66"
6) "v6"
# 从左边开始,删除列表中指定数量的指定元素
127.0.0.1:6379> lrem k2 1 v55
(integer) 1
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v4"
3) "v5"
4) "v66"
5) "v6"
127.0.0.1:6379> lrem k2 1 v555
(integer) 0
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v4"
3) "v5"
4) "v66"
5) "v6"
# 将列表指定下标位置替换成指定value
127.0.0.1:6379> lset k2 3 v7
OK
127.0.0.1:6379> lrange k2 0 -1
1) "v1"
2) "v4"
3) "v5"
4) "v7"
5) "v6"

数据结构
List 的数据结构为快速链表quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是压缩列表ziplist。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
Redis 将链表和ziplist 结合起来组成了 快速链表,也就是将多个ziplist使用双向指针串起来使用,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

4. Set

set 对外提供的功能与list类似,是一个列表的功能,区别就是set 是自动去重的,并且是String 类型的无序集合,它底层其实是一个value 为null 的hash表。所以添加、删除、查找的复杂度都是O(1)。

常用命令

127.0.0.1:6379> keys *
1) "k1"
2) "k2"
127.0.0.1:6379> flushdb
OK
# 将一个或多个元素添加到集合中,重复的元素会被忽略
127.0.0.1:6379> sadd k1 v1 v2 v3 v4 v2
(integer) 4
# 查看集合中的元素
127.0.0.1:6379> smembers k1
1) "v3"
2) "v2"
3) "v4"
4) "v1"
# 判断集合key中是否包含该value
127.0.0.1:6379> sismember k1 v1
(integer) 1
127.0.0.1:6379> sismember k1 v9
(integer) 0
# 返回集合元素个数
127.0.0.1:6379> scard k1
(integer) 4
# 删除集合中的一个或多个元素,不存在的元素被忽略
127.0.0.1:6379> srem k1 v1 v8
(integer) 1
127.0.0.1:6379> smembers k1
1) "v3"
2) "v2"
3) "v4"
# 随机从集合中吐出元素,值吐完了键也没了
127.0.0.1:6379> spop k1 1
1) "v4"
127.0.0.1:6379> spop k1 2
1) "v2"
2) "v3"
127.0.0.1:6379> spop k1 2
(empty array)
127.0.0.1:6379> spop k1 1
(empty array)
127.0.0.1:6379> exists k1
(integer) 0
127.0.0.1:6379> sadd k1 v1 v2 v3 v4
(integer) 4
127.0.0.1:6379> smembers k11
(empty array)
127.0.0.1:6379> smembers k1
1) "v3"
2) "v2"
3) "v4"
4) "v1"
# 随机从集合中取出n个元素,元素不会从集合中删除
127.0.0.1:6379> srandmember k1 2
1) "v2"
2) "v1"
127.0.0.1:6379> srandmember k1 2
1) "v3"
2) "v4"
127.0.0.1:6379> srandmember k1 2
1) "v2"
2) "v3"
127.0.0.1:6379> sadd k2 v6 v7 v8
(integer) 3
# 把集合中一个指定的值移动到另外一个集合中
127.0.0.1:6379> smove k1 k2 v1
(integer) 1
127.0.0.1:6379> smembers k1
1) "v3"
2) "v2"
3) "v4"
127.0.0.1:6379> smembers k2
1) "v7"
2) "v1"
3) "v6"
4) "v8"
127.0.0.1:6379> sadd k1 v6 v8
(integer) 2
127.0.0.1:6379> smembers k2
1) "v7"
2) "v1"
3) "v6"
4) "v8"
127.0.0.1:6379> smembers k1
1) "v3"
2) "v2"
3) "v6"
4) "v4"
5) "v8"
# 返回两个集合的交集元素
127.0.0.1:6379> sinter k1 k2
1) "v6"
2) "v8"
# 返回两个集合的并集
127.0.0.1:6379> sunion k1 k2
1) "v7"
2) "v3"
3) "v6"
4) "v8"
5) "v2"
6) "v1"
7) "v4"
# 返回两个集合的差集元素(k1中的元素但不包含在k2中的元素)
127.0.0.1:6379> sdiff k1 k2
1) "v2"
2) "v3"
3) "v4"

数据结构
set 数据结构是dict字典,字典是用哈希表实现的

5. Hash

hash 是一个键值对集合。它是一个String类型的field和value的映射表,特别适用存储对象。

常用命令

# 给集合中添加数据
127.0.0.1:6379> hset user:id name jack age 18 sex f
(integer) 3
# 查看某个属性的值
127.0.0.1:6379> hget user:id name
"jack"
127.0.0.1:6379> hget user:id age
"18"
127.0.0.1:6379> hget user:id sex
"f"
# 批量插入字段
127.0.0.1:6379> hmset user:id height 45 weight 50
OK
# 判断field在哈希表中是否存在
127.0.0.1:6379> hexists user:id height
(integer) 1
127.0.0.1:6379> hexists user:id weight
(integer) 1
# 列出哈希表中所有的field
127.0.0.1:6379> hkeys user:id
1) "name"
2) "age"
3) "sex"
4) "height"
5) "weight"
# 列出哈希集合中所有的value
127.0.0.1:6379> hvals user:id
1) "jack"
2) "18"
3) "f"
4) "45"
5) "50"
# 为哈希表中某个field 增量指定数值 加就写整数,减就写负数
127.0.0.1:6379> hincrby user:id age 10
(integer) 28
127.0.0.1:6379> hincrby user:id age
(error) ERR wrong number of arguments for 'hincrby' command
127.0.0.1:6379> hincrby user:id age 1
(integer) 29
127.0.0.1:6379> hincrby user:id age -10
(integer) 19
# 新增field ,当且仅当field不存在
127.0.0.1:6379> hsetnx user:id name lucy
(integer) 0
127.0.0.1:6379> hsetnx user:id color red
(integer) 1

数据结构
hash类型对应的数据结构有两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用 hashtable。

6. Zset

zset 是一个有序的,没有重复元素的字符串集合。它的每个元素都关联了一个评分(score),评分是可以重复的,可以用来排序使用。

常用命令

# 集合中添加数据
127.0.0.1:6379> zadd topn 200 java 300 c++ 400 mysql 500 python
(integer) 4
# 查询集合汇总数据
127.0.0.1:6379> zrange topn 0 -1
1) "java"
2) "c++"
3) "mysql"
4) "python"
# 查询集合中数据并且带上分数
127.0.0.1:6379> zrange topn 0 -1 withscores
1) "java"
2) "200"
3) "c++"
4) "300"
5) "mysql"
6) "400"
7) "python"
8) "500"
# 查询 300 -500 分之间的所有元素,包含300和500 默认正序
127.0.0.1:6379> zrangebyscore topn 300 500 withscores
1) "c++"
2) "300"
3) "mysql"
4) "400"
5) "python"
6) "500"
# 查询 300 -500 分之间的所有元素,包含300和500 倒序
127.0.0.1:6379> zrevrangebyscore topn 500 300 withscores
1) "python"
2) "500"
3) "mysql"
4) "400"
5) "c++"
6) "300"
# 给集合中的某个元素加分数
127.0.0.1:6379> zincrby topn 50 java
"250"
# 删除某个元素
127.0.0.1:6379> zrem topn java 
(integer) 1
# 统计400-500分数之间元素的个数
127.0.0.1:6379> zcount topn 400 500
(integer) 2
# 查看元素在集合中的索引位置
127.0.0.1:6379> zrank topn mysql
(integer) 1
127.0.0.1:6379> zrange topn 0 -1
1) "c++"
2) "mysql"
3) "python"
127.0.0.1:6379> zrange topn 0 -1 withscores
1) "c++"
2) "300"
3) "mysql"
4) "400"
5) "python"
6) "500"
127.0.0.1:6379> zcount topn 300 500
(integer) 3
127.0.0.1:6379> zrank topn python
(integer) 2

数据结构
zset 底层使用了两个数据结构
1)hash,它的作用就是关联value和权重score,保障value 的唯一性,可以通过元素value找到相应的score
2)跳跃表,它的作用就是给元素value排序,根据score的范围获取元素列表,不同于普通的链表,它会分层进行查找,不会按顺序一个一个查找,效率要高于普通链表

五、配置文件详解

1. Units 单位

配置大小单元,开头定义了一些基本的度量单位,只支持 bytes ,不支持 bit ,大小写不敏感。

# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

2. INCLUDES

一些通用的配置可以写到其他地方,然后在这里引入进来

################################## INCLUDES ###################################

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Note that option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf

3. NETWORK 网络

# By default, if no "bind" configuration directive is specified, Redis listens
# for connections from all available network interfaces on the host machine.
# It is possible to listen to just one or multiple selected interfaces using
# the "bind" configuration directive, followed by one or more IP addresses.
# Each address can be prefixed by "-", which means that redis will not fail to
# start if the address is not available. Being not available only refers to
# addresses that does not correspond to any network interfece. Addresses that
# are already in use will always fail, and unsupported protocols will always BE
# silently skipped.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1     # listens on two specific IPv4 addresses
# bind 127.0.0.1 ::1              # listens on loopback IPv4 and IPv6
# bind * -::*                     # like the default, all available interfaces
#
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only on the
# IPv4 and IPv6 (if available) loopback interface addresses (this means Redis
# will only be able to accept client connections from the same host that it is
# running on).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT OUT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bind 127.0.0.1 -::1   # 限定只能本机访问,如果需要远程被访问就需要注释掉

# Protected mode is a layer of security protection, in order to avoid that
# Redis instances left open on the internet are accessed and exploited.
#
# When protected mode is on and if:
#
# 1) The server is not binding explicitly to a set of addresses using the
#    "bind" directive.
# 2) No password is configured.
#
# The server only accepts connections from clients connecting from the
# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
# sockets.
#
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode yes  # 开启保护模式,只能本机进行访问,如果需要远程访问,设置为 no

# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379 # 端口号,默认6379

# TCP listen() backlog.
#
# In high requests-per-second environments you need a high backlog in order
# to avoid slow clients connection issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to get the desired effect.
tcp-backlog 511  # 设置tcp的backlog值,backlog 其实是一个连接队列,backlog队列总和=未完成三次握手队列+已完成三次握手队列,在高并发环境下需要一个高backlog值来避免慢客户端连接问题

# Unix socket.
#
# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
# unixsocket /run/redis.sock
# unixsocketperm 700

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0  # redis连接的过期时间,0代表永不超时,如果设置了时间,则时间结束后操作需要重新连接,单位是秒

# TCP keepalive.
#
# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
# of communication. This is useful for two reasons:
#
# 1) Detect dead peers.
# 2) Force network equipment in the middle to consider the connection to be
#    alive.
#
# On Linux, the specified value (in seconds) is the period used to send ACKs.
# Note that to close the connection the double of the time is needed.
# On other kernels the period depends on the kernel configuration.
#
# A reasonable value for this option is 300 seconds, which is the new
# Redis default starting with Redis 3.2.1.
tcp-keepalive 300 # 检测心跳时间,默认300秒,每隔300秒检测一下TCP连接是否活跃,如果不活跃则会释放连接

4. GENERAL 通用

# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
# When Redis is supervised by upstart or systemd, this parameter has no impact.
daemonize no # 是否后台进程,设置为yes,可以后台启动

# If you run Redis from upstart or systemd, Redis can interact with your
# supervision tree. Options:
#   supervised no      - no supervision interaction
#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode
#                        requires "expect stop" in your upstart job config
#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
#                        on startup, and updating Redis status on a regular
#                        basis.
#   supervised auto    - detect upstart or systemd method based on
#                        UPSTART_JOB or NOTIFY_SOCKET environment variables
# Note: these supervision methods only signal "process is ready."
#       They do not enable continuous pings back to your supervisor.
#
# The default is "no". To run under upstart/systemd, you can simply uncomment
# the line below:
#
# supervised auto

# If a pid file is specified, Redis writes it where specified at startup
# and removes it at exit.
#
# When the server runs non daemonized, no pid file is created if none is
# specified in the configuration. When the server is daemonized, the pid file
# is used even if not specified, defaulting to "/var/run/redis.pid".
#
# Creating a pid file is best effort: if Redis is not able to create it
# nothing bad happens, the server will start and run normally.
#
# Note that on modern Linux systems "/run/redis.pid" is more conforming
# and should be used instead.
pidfile /var/run/redis_6379.pid  # 存储redis进程号的文件

# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing) 
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice # 日志级别,有4个级别 debug  verbose(类似java中info级别) notice(生产上一般用这个) warning

# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile "" # 设置日志的输出路径,可以自己设置 比如/dev/null

# To enable logging to the system logger, just set 'syslog-enabled' to yes,
# and optionally update the other syslog parameters to suit your needs.
# syslog-enabled no

# Specify the syslog identity.
# syslog-ident redis

# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
# syslog-facility local0

# To disable the built in crash log, which will possibly produce cleaner core
# dumps when they are needed, uncomment the following:
#
# crash-log-enabled no

# To disable the fast memory check that's run as part of the crash log, which
# will possibly let redis terminate sooner, uncomment the following:
#
# crash-memcheck-enabled no

# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16 # 默认有16个库

5. SECURITY 安全

# IMPORTANT NOTE: starting with Redis 6 "requirepass" is just a compatibility
# layer on top of the new ACL system. The option effect will be just setting
# the password for the default user. Clients will still authenticate using
# AUTH <password> as usually, or more explicitly with AUTH default <password>
# if they follow the new protocol: both will work.
#
# The requirepass is not compatable with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.
#
requirepass password # 设置密码,默认是没密码的

6. CLIENTS 客户

################################### CLIENTS ####################################

# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# IMPORTANT: When Redis Cluster is used, the max number of connections is also
# shared with the cluster bus: every node in the cluster will use two
# connections, one incoming and another outgoing. It is important to size the
# limit accordingly in case of very large clusters.
#
maxclients 10000 # 设置redis同时可以与多少个客户端进行连接,达到了限制会拒绝新的连接请求,返回 max number of clients reached

7. 内存管理

设置redis可以使用的内存量,一旦到达内存使用上限,redis将试图移除内部数据,移除规则可以通过 maxmemory-policy 来指定。建议必须设置,否则内存占满,造成服务宕机。
如果redis无法根据移除规则来移除内存中的数据,或者设置了不允许移除,那么redis则会针对那些需要申请内存的指令返回错误信息,比如ser、lpush等指令。
但是对于无内存申请的指令,仍然会正常响应,比如get。如果你的redis是主redis,那么在设置内存使用上限时,需要预留一些内存空间给同步队列缓存,只有在设置不移除的情况下才不会考虑这个因素

maxmemory <bytes>
# volatile-lru -> 使用LRU 算法移除key,只对设置了过期时间的key生效(最近最少使用)
# allkeys-lru -> 在所有集合key中,使用LRU算法移除key
# volatile-lfu -> 使用LFU 算法移除key,只对设置了过期时间的key生效
# allkeys-lfu -> 在所有集合key中,使用LFU算法移除key
# volatile-random -> 在过期集合中移除随机的key,只对设置了过期时间的键
# allkeys-random -> 在所有集合key中,移除随机的key
# volatile-ttl -> 移除最近要过期的key
# noeviction -> 不进行移除,针对写操作,只是返回错误信息
maxmemory-policy noeviction

设置样本数量。LRU算法与最小TTL 算法都并非是准确的算法,而是估算值,所以你可以设置样本的大小。redis默认会检查这么多个key并且其中LRU的那个。
一般设置3-7的数字,数值越小样本越不准确,但性能消耗越小。

maxmemory-samples 5

六、Redis 的发布和订阅

Redis 发布订阅是一种消息通信模式:发送者发送消息,订阅者接收消息。Redis 客户端可以订阅任意数量的频道。

相关命令实现
发布者:向频道’channel_a’发送一条"helloworld"消息

127.0.0.1:6379> publish channel helloworld
(integer) 0
127.0.0.1:6379> publish channel_a helloworld
(integer) 1

订阅者:订阅’channel_a’的消息

127.0.0.1:6379> subscribe channel_a
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel_a"
3) (integer) 1
1) "message"
2) "channel_a"
3) "helloworld"

七、Redis6 中新的数据类型

Bitmaps

现代计算机用二进制(位)作为信息的基础单位,1字节=8bit。
例如:“abc”,计算机底层存储时用二进制表示,"abc"对应的ASCII码分别是97,98,99,对应二进制分别为 01100001,01100010,01100011
合理的使用操作位能够有效的提高内存使用率和开发效率。
Redis 提供了 Bitmaps 这个数据类型可以实现对位的操作:
1)Bitmaps 本身不是一种数据类型,实际上它就是字符串,但是它可以对字符串的位进行操作。
2)Bitmaps 单独提供了一套命令,我们可以把Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。

Bitmaps 与 set 对比
在用户活跃率大的时候用bitmaps可以极大的节省内存空间。

setbit
offset:偏移量从0开始
value:偏移量的值(0或1)
缺点:偏移量特别大的时候,整个初始化执行过程会比较慢,可能会造成redis的阻塞
在这里插入图片描述
getbit
获取偏移量的值
在这里插入图片描述
bitcount
统计字符串从start字节到end字节bit值为1的数量。
例如:字符串abc的二进制排列 01100001,01100010,01100011
先按照字符串abc去设置bit位,然后查看字符串abc不同字节中的bit位为1的数量。
start 和 end 中 -1表示最后一位,-2表示倒数第二个位
在这里插入图片描述
bitop
bitop 是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中
在这里插入图片描述

HyperLogLog

redis HyperLogLog 是用来做基数统计的算法,特点就是在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的,并且是很小的。在redis中,每个HyperLogLog 键只需要花费12KB内存,就可以计算接近 2的64次方个不同元素的基数。
基数:计算不重复元素的数量

pfadd
添加元素到指定HyperLogLog中
在这里插入图片描述
pfcount
计算HyperLogLog中的基数
在这里插入图片描述
pfmerge
两个集合中的基数合并,保存到destkey中,会去除重复的元素
在这里插入图片描述

Geospatial

Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的 二维坐标,在地图上就是经纬度。redis 基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度hash等常见操作

geoadd
两极无法直接添加,一般会下载城市数据,直接通过java程序一次性导入
有效的精度从-180到180,有效的维度从-85.05112878 到 85.05112878。当坐标位置超出范围会返回一个错误,已经添加的数据是无法再次添加的
在这里插入图片描述
geopos
取得某个元素的经纬度
在这里插入图片描述
geodist
获取两个位置之间的直线距离,单位默认米,可以自己制定。
m - 米
km - 千米
mi - 英里
ft - 英尺
在这里插入图片描述
georadius
以给定的经纬度为中心,找出某一半径内的元素
在这里插入图片描述

八、Jedis操作 Redis6

1、创建maven项目
2、引入jedis依赖

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

3、jedis 部分API初体验

public class JedisTest {
    private static Jedis jedis;
    static {
        jedis = new Jedis("xx.xxx.xxx.xxx", 6379);
        jedis.auth("xxxxxx");
    }

    public static void main(String[] args) {
        // 测试redis连接
        System.out.println(jedis.ping());
        System.out.println("-------------分割线-------------");
        // 查看某个键的过期时间
        Long ttl = jedis.ttl("name");
        System.out.println(ttl);
        System.out.println("-------------String-------------");
        jedis.set("k1", "v1");
        jedis.mset("k2", "v2", "k3", "v3");
        System.out.println("-------------分割线-------------");
        // 匹配所有key
        Set<String> keys = jedis.keys("*");
        for (String key : keys) {
            System.out.println("key:" + key);
        }
        System.out.println("-------------分割线-------------");
        System.out.println(jedis.get("k1"));
        List<String> mget = jedis.mget("k1", "k2", "k3");
        System.out.println(mget);
        System.out.println("-------------list-------------");

        jedis.lpush("animals", "dog", "cow", "duck");
        System.out.println(jedis.lrange("animals", 0, -1));
        System.out.println("-------------set-------------");
         jedis.sadd("fruits", "apple","banana","apple");
        System.out.println(jedis.smembers("fruits"));
        jedis.srem("fruits", "apple");
        System.out.println("-------------hash-------------");
        jedis.flushDB();
        jedis.hset("user:1001", "name", "jack");
        jedis.hset("user:1001", "age", "18");
        System.out.println(jedis.hget("user:1001", "name"));
        Map<String, String> map = new HashMap<String,String>(16);
        map.put("tel", "123456789");
        map.put("add", "beijing");
        jedis.hmset("user:1001", map);
        System.out.println(jedis.hmget("user:1001", "name", "age", "tel", "add"));
        System.out.println("-------------zset-------------");
        jedis.zadd("language", 80, "java");
        jedis.zadd("language", 90, "python");
        jedis.zadd("language", 70, "c++");
        System.out.println(jedis.zrange("language", 0, -1));
    }
}

4、模拟验证码发送

public class Verify {

    private static Jedis jedis;

    static {
        jedis = new Jedis("xx.1xx.1xx.1x2", 6379);
        jedis.auth("xxxxxxx");
    }


    public static void main(String[] args) {

//        setRedis("xxxxxxxxxx");

        String codeKey = "verify:" + "xxxxxxxxx" + ":code";
        String code = jedis.get(codeKey);
        System.out.println(code); // 893175
        if ("893175".equals(code)){
            System.out.println("11");
        }

    }


    public static void setRedis(String phone){

        // 生成key
        String phoneKey = "verify:" + phone + ":phone";
        String codeKey = "verify:" + phone + ":code";

        String count = jedis.get(phoneKey);

        if (count == null){
            jedis.setex(phoneKey, 24*60*60, "1");
        }else if (Integer.parseInt(count) < 3){
            jedis.incr(phoneKey);
        }else {
            System.out.println("该手机今天访问超过三次");
            jedis.close();
            return;
        }
        jedis.setex(codeKey, 120 , getCode());
        jedis.close();

    }


    /**
     * 生成验证码
     * @return
     */
    public static String getCode(){
        Random random = new Random();
        String code = "";
        for (int i = 0; i < 6; i++) {
            int num = random.nextInt(10);
            code += num;
        }
        return code;
    }


}

九、SpringBoot 整合 Redis

1、引入依赖

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>

		<!--Jackson包-->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-core</artifactId>
			<version>2.9.0</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.9.0</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
			<version>2.9.0</version>
		</dependency>

2、配置

spring.redis.host=xx.1x1.1xx.xx2
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=xxxxxx
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    public CacheManager cacheManager(RedisConnectionFactory factory){
        RedisSerializer<String> serializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer<Object> jackson = new Jackson2JsonRedisSerializer<>(Object.class);
        // 解决查询缓存转换异常问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson.setObjectMapper(om);
        // 配置序列化(解决乱码问题) , 过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();

        return cacheManager;
    }

}

3、测试

@SpringBootTest
class RedisDemoApplicationTests {
	@Autowired
	private RedisTemplate redisTemplate;

	@Test
	void contextLoads() {
	}
	@Test
	void testRedis(){

		ValueOperations ops = redisTemplate.opsForValue();

		ops.set("name", "jack");
		System.out.println(ops.get("name"));


	}

}

十、Redis 事务和锁机制

1、简介
redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
redis 事务的主要作用就是串联多个命令,防止别的命令插队。

2、Multi、Exec、discard
从输入multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入exec后,redis 会将之前的命令队列中的命令依次执行。组队过程中可以通过discard来放弃组队。
在这里插入图片描述
3、事务的错误处理
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚
在这里插入图片描述
4、乐观锁和悲观锁
悲观锁:每次操作之前都会上锁,防止别人修改。
乐观锁:每次操作都不会上锁,数据会有个版本号,每次拿数据的时候连带版本号一起拿,等操作完之后,把之前拿到的版本号和目前最新的版本号比较,如果是一致则操作成功,否则失败。适用于多读的场景,提高吞吐量。Redis 就是利用这种 check-and-set 机制实现事务的

5、watch key [key …]
在执行 multi 之前,先执行 watch key [key …] ,可以监视一个或多个 key,如果在事务执行之前这个或这些key被其他命令所改动,那么事务将被打断。

事务A
在这里插入图片描述

事务B:由于watch的key被事务A给变更了,所以事务B被打断
在这里插入图片描述
6、unwatch
无效watch命令对所有key的监视
如果在执行watch命令后,exec和discard 先执行了,那么可以不用执行unwatch
在这里插入图片描述
7、redis 事务三特性
单独的隔离操作:事务中的所有命令都会序列化,按顺序的执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

Redis事务-秒杀案例

public class SecKill_redis {

    public static void main(String[] args) {
        Jedis jedis =new Jedis("192.168.242.110",6379);
        System.out.println(jedis.ping());
        jedis.close();
    }

    //秒杀过程
    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.242.110",6379);

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



        //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) < 1){
            System.out.println("秒杀结束,请下次参与");
            jedis.close();
            return false;
        }

        //7 秒杀过程
        //7.1库存-1
        jedis.decr(kcKey);
        //7.2 把秒杀成功的用户添加到清单里面
        jedis.sadd(userKey,uid);
        System.out.println("用户" + uid + "秒杀成功");
        jedis.close();
        return true;
    }
}

使用事务解决超卖问题

//秒杀过程
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.242.110",6379);

    //通过连接池获取连接redis的对象
    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) < 1){
        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("用户" + uid + "秒杀成功");
    jedis.close();
    return true;
}

使用lua脚本解决原子性问题

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 持久化

Redis 提供了两种不同形式的持久化方式

RDB(Redis DataBase)

1、rdb 是什么
在指定的时间间隔内将内存中的数据集快照写入磁盘,这里的快照就是我们平时说的 Snapshot 快照。它恢复时是将快照文件直接读到内存里。

2、备份是如何执行的
redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对数据恢复的完整度不是非常敏感,那RDB 方式要比AOF方式更加的高效。RDB的缺点就是最后一次持久化后的数据可能丢失。

3、Fork 是什么
① Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
② 在linux程序中,fork() 会产生一个和父进程完成相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux 中引入了“写时复制技术”。
③ 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

4、RDB持久化流程
① redis 会fork(),创建一个子进程
② 子进程将内存中的数据集快照写到一个临时文件中
③ 持久化结束后,会将临时文件替换上次持久化好的文件

5、相关配置项

# 3600秒(1小时)至少1个key发生变化就会进行持久化
# save 3600 1
# 300秒(5分钟)至少100个key发生变化就会进行持久化
# save 300 100
# 60秒(1分钟)至少10000个key发生变化就会进行持久化
# save 60 10000

# RDB持久化文件的文件名
dbfilename dump.rdb

# 存放持久化文件的位置
dir ./

# 当redis无法写入磁盘的话,直接关掉redis的写操作,推荐yes
stop-writes-on-bgsave-error yes

# 对于存储在磁盘中的快照,可以设置是否进行压缩存储,如果是的话,redis会采用LZF算法进程压缩。推荐yes
rdbcompression yes

# 检查数据完整性,在存储快照后,redis使用CRC64算法来进行数据校验,推荐yes
rdbchecksum yes

6、如何触发RDB快照
① 配置文件中配置
② 执行命令 save :save 时只管保存,其它全部阻塞。手动保存,不建议
③ 执行命令 bgsave:redis 会在后台异步进行快照操作,快照的同时还可以响应客户端请求。可以通过 lastsave 命令获取最后一次成功执行快照的时间
④ 执行 flushall 命令,也会产生 dump.rdb文件,但是里面是空的,无意义。

7、RDB的备份及恢复
先通过 config get dir 查询rdb文件的目录,拷贝rdb文件到别的地方。
rdb的恢复

  • 关闭redis
  • 先把备份的文件拷贝的redis工作目录下
  • 启动redis,备份数据会直接加载

8、RDB 的优缺点

优点

  • 适合大规模的数据恢复
  • 比较适合对数据完整性和一致性要求不高的
  • 节省磁盘空间
  • 恢复速度快

缺点

  • Fork 的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  • 虽然redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
  • 在备份周期在一定间隔时间做一次备份,所以如果redis 意外down掉的话,就会丢失最后一次快照后的所有修改

9、如何停止
动态停止RDB:redis-cli config set save “”

AOF(Append Of File)

1、aof 是什么
以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据(根据日志文件的内存将里面的指令从头到尾执行一遍)

2、aof 持久化流程
1)客户端的请求写命令会被append追加到AOF缓冲区内
2)AOF缓冲区根据AOF持久化策略[always、everysec、no]将操作sync同步到磁盘的AOF文件中
3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
4)redis 服务重启时,会重新加载AOF文件中的写操作达到数据恢复的目的

3、AOF 如何开启
aof默认是不开启的,我们可以通过修改redis配置文件开启。
如果AOF和RDB同时开启,系统默认取AOF的数据

# 默认是关闭的,开启的话改成yes即可
appendonly no

# aof文件名
appendfilename "appendonly.aof"

# aof文件保存的位置与rdb文件是一样的

4、AOF 数据恢复
AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作同RDB一致,都是拷贝备份文件,需要恢复时再拷贝到redis工作目录下,启动redis加载文件恢复数据。

正常恢复
1)开启appendonly yes
2)将有数据的aof文件复制一份到工作目录中
3)启动redis即可加载数据

备份文件损坏
1)开启appendonly yes
2)如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof 进行恢复
3)备份被写坏的AOF文件
4)启动redis即可加载数据

5、AOF 同步频率设置

# appendfsync always  # 始终同步,redis每执行一次写命令,就立刻记录到日志中,性能较差,数据完整性高
appendfsync everysec # 每秒同步一次,如果宕机,最后一秒的数据丢失
# appendfsync no   # redis不主动进行同步,把同步时机交给操作系统

6、Rewrite 压缩
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设置的阈值时,redis就会启动AOF文件的压缩内容,只保留可以恢复数据的最小指令集。可以使用命令bgrewriteaof
在这里插入图片描述
1)重写原理
aof文件过大时,redis会fork出一个新的进程来将文件重写(先写到临时文件,最后替换掉),redis 4.0 版本后的重写,就是把rdb的快照,以二进制的形式附加在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。

no-appendfsync-on-rewrite no
此配置如果开启,no改为yes,写指令不会写入aof文件,只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机,会丢失这段时间的缓存数据,损失了安全性,换来了性能
如果不开启,就会把指令刷到磁盘里,但是遇到重写操作,可能会发生阻塞。

重写的触发时机
redis会记录上次重写时的aof文件大小,默认配置是当aof文件大小是上次重写后大小的一倍且文件大于64M时触发。

# 设置重写的基准值,文件达到100%时开始重写(aof文件大小是上次重写后大小的一倍)
auto-aof-rewrite-percentage 100
# 另外一个基准值,文件最小要达到64M,才会触发
auto-aof-rewrite-min-size 64mb

7、重写的流程
1)bgrewriteaof 触发重写,判断是否当前有bgsave或bgrewriteaof 在运行,如果有,则等待该命令结束后再继续执行
2)主进程fork出子进程执行重写操作,保证主进程不会阻塞
3)子进程遍历redis内存中的数据到临时文件,客户端的写请求同时写入aof_buf 缓冲区和aof_rewrite_buf 重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失
4)子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息,主进程把aof_rewrite_buf 中的数据写入到新的AOF文件中
5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写

8、AOF 的优缺点
优点
1)备份机制更稳健,丢失数据概率低
2)可读的日志文件,通过操作aof 稳健,可以处理误操作

缺点
1)比起RDB占用更多的磁盘空间
2)恢复备份速度要慢
3)每次写都同步的话,有一定的性能压力
4)存在个别潜在bug,导致不能恢复

总结

1、选择哪一种持久化方式
官方推荐两个都启用,如果对数据不敏感,可以单独选用RDB,不建议单独开启AOF,因为会出现未知BUG,如果只是做纯内存缓存,可以都不用。

十二、Redis 主从复制

1、什么是redis主从复制
主机数据更新后根据配置和策略,自动同步到备机的 master/slaver 机制,Master 以写为主,Slave 以读为主。

2、有什么作用
1)读写分离,性能扩展
2)容灾快速恢复

3、如何配置redis的主从复制
① 创建文件夹用于存放redis配置文件

cd /
mkdir myredis

② 复制配置到新创建的文件夹中,由于只有一台服务器,我们建三个配置文件,模拟一主二从

cp /usr/local/bin/redisconf/redis.conf ./redis6379.conf
cp ./redis6379.conf ./redis6380.conf
cp ./redis6379.conf ./redis6381.conf

③ 修改6380和6381两个配置中的端口号,RDB备份文件名,pidfile的名称

vim ./redis6380.conf
port 6380
pidfile /var/run/redis_6380.pid
dbfilename dump6380.rdb

vim ./redis6381.conf
port 6381
pidfile /var/run/redis_6381.pid
dbfilename dump6381.rdb

④ 启动三个服务
启动后,利用客户端根据端口登录redis(redis-cli -p port),执行info replication 命令可以查看主从复制的相关信息。
这时候这三台redis服务还不是主从关系。

⑤ 配置主从关系
登录redis从服务器,执行命令 slaveof <ip> <port>,让当前redis服务成为某个实例的从服务器 ,
如果redis配置了密码需要在从库的conf里添加masterauth 密码 不然配置从库的时候从库显示有主库 主库却看不见从库。
在这里插入图片描述

4、测试
master 可以写数据
在这里插入图片描述
slave 只能读数据,不能写
在这里插入图片描述

5、主从复制的复制原理和一主二仆
一主二从中,如果slave 挂了,重启之后会变成master,需要重新执行命令 slaveof <ip> <port>,让当前redis服务成为某个实例的从服务器。master 中的数据从头开始复制全部复制到重启的服务器中。
主服务器master 挂了之后,slave服务器不会上位,还是从,主重启之后还是主。

主从复制原理
1)slave 启动成功连接到master 后会发送一个sync命令
2)master 接收到同步命令后,启动后台的存盘命令,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件(RDB文件)到salve,以完成一次同步
3)全量复制:而slave第一次同步的时候会全量同步,接收数据文件后,将其存盘并加载到内存中
4)增量复制:后续master继续将新的所有收集到的修改命令依次传给slave ,完成同步
5)但是只要是重新连接master,一次全量同步就会被自动执行。

6、薪火相传和反客为主
薪火相传:上一个slave可以是下一个slave的master。slave 同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中的下一个master,可以有效的减轻master 的写压力,去中心化降低风险。缺点是一旦链条中的一个redis服务器宕机,那么它后面的从服务器就无法同步到第一个master服务器的数据了。
如何设置薪火相传:执行命令 slaveof <ip> <port> ,让从服务器成为另外一个从服务器的slave,中途变更转向,会清楚之前的数据,重新复制当前主机的数据。

反客为主:如果master服务器挂了,那么从服务器可以执行 slaveof no one,将从机变成主机。缺点就是要手动完成。

7、哨兵模式
1)是什么
哨兵模式可以理解为反客为主的自动版,能够后台监视主机是否故障,如果故障了根据投票数自动将从库转换成主库。
2)如何配置哨兵模式
① 模拟三个服务器,调整成一主二从模式
② 创建一个文件 sentinel.conf 文件,注意这边文件名不能错
③ sentinel.conf 文件中添加内容:sentinel monitor mymaster 127.0.0.1 6379 1,其中mymaster 为监控对象起的服务器名称,1 的意思是至少有多少个哨兵同意迁移的数量.注意如果redis配置了密码的,需要在哨兵配置文件里 配置 sentinel auth-pass mymaster xxxxxx
④ 启动哨兵 redis-sentinel /myredis/sentinel.conf ,如果要让哨兵在后台运行配置文件中加上 daemonize yes,这里为了看日志就不加了
在这里插入图片描述
⑤ master 如果宕机了哨兵会从slave中选一个做master,另外一个salve会作为新的master的从服务,原主重启之后不会称为master,而是称为新的master的从服务器。
在这里插入图片描述
⑥ redis 的主从缺点
会有复制延时的情况,写都在master服务器上,从服务器要同步主的数据,势必会有时间上的延时,哪怕这个时间很短。

⑦ 故障恢复
master 选举:master宕机后,sentinel 会从slave服务中选一个作为新的master,选择条件依次为 优先级靠前的 > 选择偏移量最大的 > 选择runid最小的从服务
sentinel 会向原master的slave发送 slaveof 新主 的命令,复制新master的数据
原master如果故障恢复重启了,sentinel 会向其发送slaveof 新主的命令,复制新主的数据

runid:每个redis实例启动后会随机生成一个 40位的runid
优先级:redis.conf中的配置,数值越小优先级越高
在这里插入图片描述

十三、Redis cluster 集群

1、为什么需要集群
前面我们提到的主从模式或者薪火相传模式,都是master负责写操作,如果master容量满了,服务就不可用了,还有就是并发比较大的时候,势必会造成阻塞等待,另外,主从中master宕机,从服务器选举成为新master,势必要修改我们应用程序中配置的host和port等信息。这时候我们就需要通过集群来解决redis服务高可用的问题。

2、什么是redis集群
redis 集群实现了redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
redis 集群通过分区来提供一定程度的可用性,即使集群中部分节点失效,集群也可以提供服务。

3、如何搭建cluster 集群
1)由于只有一台服务器,只能通过多个配置不同端口来模拟,修改配置

#端口配置 6379,6380,6381,6389,6390,6391
port  6379
#改为其他节点机器可访问的ip 可以使用                                                
bind 0.0.0.0 
#redis后台运行                                    
daemonize    yes                               
pidfile  /var/run/redis_6379.pid
dbfilename "dump6379.rdb"
dir "/myredis"
# 集群相关配置          
cluster-enabled  yes                         
cluster-config-file  nodes_6379.conf   
cluster-node-timeout  15000               

2)启动各个节点
在这里插入图片描述
3)执行启动集群命令,由于我用的redis版本比较新,里面集成了ruby,所以不需要安装

# 进入到redis目录的src下
cd /app/redis-6.2.7/src/
# 启动集群
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6389 127.0.0.1:6390 127.0.0.1:6391 --cluster-replicas 1 -a xxxxxxxx

在这里插入图片描述
4)连接集群
添加参数 -c 表示集群策略连接

redis-cli -c -p 6379 -a xxxxxxx
# 查看节点信息
cluster nodes

在这里插入图片描述
5)redis cluster 是如何分配这六个节点的
我们刚才启动集群中有一个参数 --cluster-replicas 1,表示希望为集群中的每个master创建一个slave。分配原则就是尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。

6)什么是slots
前面我们启动集群的时候,最后会显示 All 16384 slots coverd.,一个redis集群包含16384个插槽(hash slot),数据库中的每个键就属于16384个插槽的其中一个。
集群使用公式CRC16(key)%16384 来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16校验和。
集群中的每个节点负责处理一部分插槽:比如 节点A负责 0-5460,节点B负责5461-10922,节点C负责10923-16383。

7)集群中基本命令

127.0.0.1:6379> keys *
(empty array)
# 集群中设置值,如果计算的slot不是在本机会自动切换
127.0.0.1:6379> set k1 v1
-> Redirected to slot [12706] located at 127.0.0.1:6381
OK
# 设置多个值的时候,需要按组的形式操作,不然会报错
127.0.0.1:6381> mset name jack age 18 address hangzhou
(error) CROSSSLOT Keys in request don't hash to the same slot
127.0.0.1:6381> mset name jack{user} age 18{user} address hangzhou{user}
(error) CROSSSLOT Keys in request don't hash to the same slot
127.0.0.1:6381> mset name{user} jack age{user} 18 address{user} hangzhou
-> Redirected to slot [5474] located at 127.0.0.1:6380
OK
# 查看key所在的插槽
127.0.0.1:6380> cluster keyslot k1
(integer) 12706
127.0.0.1:6380> cluster keyslot user
(integer) 5474
# 计算插槽内有几个key
127.0.0.1:6380> cluster countkeysinslot 5474
(integer) 3
# 如果这个槽位不在当前机器上是查不到的
127.0.0.1:6380> cluster countkeysinslot 12706
(integer) 0
# 查看槽中的key有哪些
127.0.0.1:6380> cluster getkeysinslot 5474 3
1) "address{user}"
2) "age{user}"
3) "name{user}"
127.0.0.1:6380> 

8)故障恢复
集群中某个master宕机后,那么它的slave会成为新主,如果旧主重启了,会成为新主的slave。
那么如果集群中某一主从都宕机了,那么得根据redis.conf的一个参数 cluster-require-full-coverage 来决定,如果值是yes则整个集群不可用,如果是no则只是这部分插槽不可用,其他两个服务的插槽都可用

127.0.0.1:6380> shutdown
not connected> exit
[root@fangchaoyang ~]# redis-cli -c -p 6379 -a xxxxxx
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> cluster nodes
19acdc2a9d8b2fefc9d2d6527999c93d5b2d5717 127.0.0.1:6381@16381 master - 0 1659656093426 3 connected 10923-16383
e34443f6a4fd8c9802aa2456228dfcb3bb832d21 127.0.0.1:6379@16379 myself,master - 0 1659656092000 1 connected 0-5460
5a90be423e7b5f77dcd1bf1fc21e5e3e620ebd64 127.0.0.1:6380@16380 master,fail - 1659656045188 1659656042000 2 disconnected
da065fbe1b45288ab23bf519881089dc011b5da8 127.0.0.1:6389@16389 slave e34443f6a4fd8c9802aa2456228dfcb3bb832d21 0 1659656095436 1 connected
efc1d182f208b69f6bfdd86d8b9ac0471deb76b8 127.0.0.1:6390@16390 master - 0 1659656094431 7 connected 5461-10922
f411951878a578a8a4f2664156123e0dc789b0e9 127.0.0.1:6391@16391 slave 19acdc2a9d8b2fefc9d2d6527999c93d5b2d5717 0 1659656094000 3 connected
# 中间重启6380端口的服务
127.0.0.1:6379> cluster nodes
19acdc2a9d8b2fefc9d2d6527999c93d5b2d5717 127.0.0.1:6381@16381 master - 0 1659656227074 3 connected 10923-16383
e34443f6a4fd8c9802aa2456228dfcb3bb832d21 127.0.0.1:6379@16379 myself,master - 0 1659656224000 1 connected 0-5460
5a90be423e7b5f77dcd1bf1fc21e5e3e620ebd64 127.0.0.1:6380@16380 slave efc1d182f208b69f6bfdd86d8b9ac0471deb76b8 0 1659656224000 7 connected
da065fbe1b45288ab23bf519881089dc011b5da8 127.0.0.1:6389@16389 slave e34443f6a4fd8c9802aa2456228dfcb3bb832d21 0 1659656225000 1 connected
efc1d182f208b69f6bfdd86d8b9ac0471deb76b8 127.0.0.1:6390@16390 master - 0 1659656226000 7 connected 5461-10922
f411951878a578a8a4f2664156123e0dc789b0e9 127.0.0.1:6391@16391 slave 19acdc2a9d8b2fefc9d2d6527999c93d5b2d5717 0 1659656226070 3 connected

十四、Jedis 操作 Redis cluster 集群

public class JedisTest {
    public static void main(String[] args) {
        HostAndPort hostAndPort = new HostAndPort("4x.1xx.1xx.xxx", 6379);
        JedisCluster jedisCluster = new JedisCluster(hostAndPort);
        jedisCluster.set("b1","bv1");
        System.out.println(jedisCluster.get("b1"));
    }
}

十五、应用问题解决

1. 缓存穿透

高并发的情况下,查询一些缓存中不存在的key,这时候请求就会到数据库,数据库如果没有这个数据,就不会放到缓存中,最终大量无效请求到数据库,导致数据库崩溃。

解决方案:
1)对空值进行缓存
如果一个查询的返回结果为空(不管是数据是否不存在),我们仍然把这个空结果进行缓存,然后给这个空结果的key设置一个过期时间,最长不超过5分钟。
2)设置可访问的名单(白名单)
使用 bitmaps 类型定义一个可以访问的白名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问的id不在bitmaps里面,进行拦截,不允许访问。
3)采用布隆过滤器
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
4)进行实时监控
当发现redis的命中率开始急速降低,需要排查访问对象和访问数据,和运维人员配合,可以设置黑名单限制服务

2. 缓存击穿

某个热门key对应的数据存在,但在缓存中过期了,此时若有大量并发请求过来,这些请求就会直接请求到数据库,导致数据库宕机。

解决方案:
1)预先设置热门数据:
在 redis 高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的过期时长。
2)实时调整
现场监控哪些热门数据,实时调整key的过期时间
3)使用锁
查询一个数据的时候,发现结果为空,不直接查数据库,而是先设置这个key的排它锁,然后再去查库,其他线程如果来查就先阻塞,直到我前面查询到结果放入缓存中,其他线程再去缓存中拿数据

3. 缓存雪崩

高并发访问的时候,缓存中大量key集中失效,导致缓存中查不到数据,请求全部到数据库,导致数据库崩溃。

解决方案:
1)构建多级缓存架构
nginx缓存 ->redis缓存->ehcache缓存等
2)使用锁和队列
用加锁或者队列的方式保证不会有大量的线程对数据库一次性进行读,从而避免缓存失效大量请求到数据库,但是缺点是会导致性能低下
3)设置过期标志更新缓存
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存
4)将缓存的失效时间分散开
比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5分钟随机,这样每个缓存key的过期时间重复率就会降低,就很难引发集体失效的事件。

分布式锁的实现redis

@SpringBootTest
class RedisDemoApplicationTests {
	@Autowired
	private RedisTemplate redisTemplate;

	@Test
	void contextLoads() {
	}

	@Test
	void testRedis(){
		ValueOperations ops = redisTemplate.opsForValue();

		// uuid是为了防止释放别人的锁
		String uuid = UUID.randomUUID().toString();

		// 设置过期时间是为了,万一上锁之后宕机或者阻塞了导致锁一直不释放
		Boolean lock = ops.setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);

		if (lock){
			System.out.println("业务逻辑执行");

			Object num = ops.get("num");
			if (StringUtils.isEmpty(num)){
				return;
			}
			int number = Integer.parseInt(num + "");
			ops.set("num", ++number);


			/**
			 * 由于判断锁和删除锁的操作非原子性,可以使用lua脚本解决
			 */
//			String uuidLock = (String) ops.get("lock");
//			// 如果是自己设置的锁就释放
//			if (uuid.equals(uuidLock)){
//				// 释放锁 但是这边还有个问题?
//				redisTemplate.delete("lock");
//			}
			// lua脚本,保证原子性
			String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
			DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
			redisScript.setScriptText(script);
			// 如果不设置返回类型默认是String,与返回0会报错
			redisScript.setResultType(Long.class);
			// 执行脚本第一个参数是脚本,第二个参数是要比对的key,第三个参数是传的值
			redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);
			
		} else {
			// 重试
			try {
				Thread.sleep(100);
				testRedis();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

十六 Redis6.0的新功能

1. ACL

Redis ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。

在Redis 5版本之前,Redis 安全规则只有密码控制 还有通过rename 来调整高危命令比如 flushdb , KEYS* , shutdown 等。Redis 6 则提供ACL的功能对用户进行更细粒度的权限控制:

(1)接入权限:用户名和密码

(2)可以执行的命令

(3)可以操作的 KEY

ACL 相关命令

# 展现用户权限列表
acl list
# 查看添加权限指令类别
acl cat
# 加参数类型名可以查看类型下具体命令
acl cat string
# 查看当前用户
acl whoami
# 创建新用户,这里用的默认权限,没指定
acl setuser user
# 设置有用户名、密码、ACL权限、并启用的用户
acl setuser user on >password ~cached:* +get
# 切换用户
acl user password

在这里插入图片描述

2. IO多线程

Redis6终于支撑多线程了,告别单线程了吗?

IO多线程其实指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程

Redis 6 加入多线程,但跟 Memcached 这种从 IO处理到数据访问多线程的实现模式有些差异。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。

另外,多线程IO默认也是不开启的,需要再配置文件中配置

io-threads-do-reads yes
io-threads 4

3. 工具支持 Cluster

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

4. 其他

Redis6新功能还有:

1、RESP3新的 Redis 通信协议:优化服务端与客户端之间通信

2、Client side caching客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。

3、Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。

4、Modules API:Redis 6中模块开发进展非常大,因为为了开发复杂的功能,从一开始就用上模块。可以变成一个框架,利用来构建不同系统,而不需要从头开始写然后还要许可。一开始就是一个向编写各种系统开放的平台。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值