Redis入门篇

一、Redis安装

1、下载安装最新版的gcc编译器

代码如下(示例):

#安装C 语言的编译环境
yum install centos-release-scl scl-utils-build
yum install -y devtoolset-8-toolchain
scl enable devtoolset-8 bash
#或 直接安装
yun install gcc 
gcc --version #测试 gcc版本 

2、解压redis并make编译环境

代码如下(示例):

yum install lrzsz #安装rz
cd /opt #下载redis-6.2.1.tar.gz放/opt目录
tar -zxvf redis-6.2.1.tar.gz #解压redis文件
cd redis-6.2.1/
make #执行make命令编译环境
make install

3、前台启动(不推荐)

前台启动,命令行窗口不能关闭,否则服务器停止

redis-server

在这里插入图片描述

4、后台启动(推荐)

代码如下(示例):

cp redis.conf /etc/redis.conf #拷贝一份redis.conf到其他目录
sudo nano redis.conf #修改redis.conf(128行)文件将里面的daemonize no 改成 yes,让服务在后台启动
redis-server /etc/redis.conf #2.2.5.3.	Redis启动
redis-cli #客户端访问:redis-cli

#redis关闭
redis-cli shutdown #单实例关闭
ps -ef | grep redis#查看端口进程号
kill -9 进程号 

5、Redis介绍相关知识

Redis是单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)

串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)

(与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用)

默认16个数据库,类似数组下标从0开始,初始默认使用0号库

使用命令 select 来切换数据库。如: select 8

统一密码管理,所有库同样密码。

dbsize查看当前数据库的key的数量

flushdb清空当前库

flushall通杀全部库

二、常见五大数据类型

1、Redis 键(key)

常见命令如下(示例):

#keys *查看当前库所有key 
127.0.0.1:6379> keys *
(empty array)
#exists key判断某个key是否存在
127.0.0.1:6379> exists k1
(integer) 1
#type key 查看你的key是什么类型
127.0.0.1:6379> type k2
string
#del key 删除指定的key数据
127.0.0.1:6379> del k3
(integer) 1
#为给定的key设置过期时间(10秒)
127.0.0.1:6379> expire k1 10
(integer) 1
#ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
127.0.0.1:6379>  ttl k2
(integer) -1
#dbsize查看当前数据库的key的数量
127.0.0.1:6379> dbsize
(integer) 1
#flushdb清空当前库
127.0.0.1:6379> flushdb
OK
#flushall通杀全部库
#unlink key   根据value选择非阻塞删除

2、 Redis字符串(String)

Sring类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

常见命令如下(示例):

127.0.0.1:6379> keys *
(empty array)
#set   <key><value>添加键值对
#*NX:当数据库中key不存在时,可以将key-value添加数据库
#*XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
#*EX:key的超时秒数
#*PX:key的超时毫秒数,与EX互斥
127.0.0.1:6379> set k1 v10
OK
#get   <key>查询对应键值
127.0.0.1:6379> get k1
"v10"
127.0.0.1:6379> get k2
"v20"
#append  <key><value>将给定的<value> 追加到原值的末尾
127.0.0.1:6379> append k1 abc
(integer) 6
#strlen  <key>获得值的长度
127.0.0.1:6379> strlen k1
(integer) 6
#setnx  <key><value>只有在 key 不存在时    设置 key 的值
127.0.0.1:6379> setnx k1 v1
(integer) 0
#incr <key> 将 key 中储存的数字值增1 只能对数字值操作,如果为空,新增值为1
127.0.0.1:6379> incr k4 
(integer) 501
127.0.0.1:6379> get k4
"501"
#decr <key> 将 key 中储存的数字值减1 只能对数字值操作,如果为空,新增值为-1
127.0.0.1:6379> decr k4
(integer) 500
127.0.0.1:6379> get k4
"500"
#incrby / decrby  <key><步长>将 key 中储存的数字值增减。自定义步长。
127.0.0.1:6379> incrby k4 10
(integer) 510
#mset  <key1><value1><key2><value2>  ..... 
#同时设置一个或多个 key-value对  
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
#mget  <key1><key2><key3> .....
#同时获取一个或多个 value  
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> set name lucy
OK
#getrange  <key><起始位置><结束位置> 获得值的范围,类似java中的substring,前包,后包
127.0.0.1:6379> getrange name 0 2
"luc"
#setrange  <key><起始位置><value> 用 <value>  覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。

#setex  <key><过期时间><value>
127.0.0.1:6379> setex age 20 value30
OK

原子性:
在这里插入图片描述

所谓原子操作是指不会被线程调度机制打断的操作;

这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

(1)在单线程中, 能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。

(2)在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。

​ Redis单命令的原子性主要得益于Redis的单线程。

3、 Redis列表(List)

单键多值

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLxVDBT3-1659611338351)(.\redis-asset\image-20220727193704962.png)]

常见命令如下(示例):

#lpush/rpush  <key><value1><value2><value3> .... 从左边/右边插入一个或多个值。
127.0.0.1:6379> lpush k1 v1 v2 v3
(integer) 3
#lrange <key><start><stop> 按照索引下标获得元素(从左到右)   0左边第一个,-1右边第一个,(0-1表示获取所有)
127.0.0.1:6379> lrange k1 0 -1
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> rpush v1 v2 v3
(integer) 2
127.0.0.1:6379> lrange v1 0 -1
1) "v2"
2) "v3"
#lpop/rpop  <key>从左边/右边吐出一个值。值在键在,值光键亡。
127.0.0.1:6379> lpop k1
"v3"
127.0.0.1:6379> rpop v1
"v3"
127.0.0.1:6379> flushdb
O
127.0.0.1:6379> lpush k1 v1 v2 v3
(integer) 3
127.0.0.1:6379> rpush v11 v22 v33
(integer) 2
127.0.0.1:6379> rpoplpush k1 v11
"v1"
127.0.0.1:6379> lrange v11 0 -1
1) "v1"
2) "v22"
3) "v33"
#lindex <key><index>按照索引下标获得元素(从左到右)
127.0.0.1:6379> lindex v11 1
"v22"
#llen <key>获得列表长度 
127.0.0.1:6379> llen v11
(integer) 3
127.0.0.1:6379> lrange v11 0 -1
1) "v1"
2) "v22"
3) "v33"
#linsert <key>  before <value><newvalue>在<value>的后面插入<newvalue>插入值
127.0.0.1:6379> linsert v11 before  'v22' 'v2222'
(integer) 4
127.0.0.1:6379> lrange v11 0 -1
1) "v1"
2) "v2222"
3) "v22"
4) "v33"
#lrem <key><n><value>从左边删除n个value(从左到右)
#lset<key><index><value>将列表key下标为index的值替换成value
127.0.0.1:6379> lset v11 v1 v1111
(error) ERR value is not an integer or out of range
127.0.0.1:6379> lset v11 1 v1111
OK
127.0.0.1:6379> lrange v11 0 -1
1) "v1"
2) "v1111"
3) "v22"
4) "v33"

4、Set集合

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。

常见命令如下(示例):

#sadd <key><value1><value2> ..... 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
127.0.0.1:6379> sadd k1 v1 v2 v3
(integer) 3
#smembers <key>取出该集合的所有值。
127.0.0.1:6379> smembers k1
1) "v1"
2) "v2"
3) "v3"
#sismember <key><value>判断集合<key>是否为含有该<value>值,有1,没有0
127.0.0.1:6379> sismember k1 v1
(integer) 1
#scard<key>返回该集合的元素个数。
127.0.0.1:6379> scard k1
(integer) 3
#srem <key><value1><value2> .... 删除集合中的某个元素。
127.0.0.1:6379> srem k1 v1 v2
(integer) 2
#spop <key>随机从该集合中吐出一个值。
127.0.0.1:6379> spop k2
"v3"
#srandmember <key><n>随机从该集合中取出n个值。不会从集合中删除 。
127.0.0.1:6379> srandmember k2 2
1) "v1"
2) "v2"
#smove <source><destination>value把集合中一个值从一个集合移动到另一个集合
127.0.0.1:6379> smove k1 k2 v3
(integer) 1
#sinter <key1><key2>返回两个集合的交集元素
127.0.0.1:6379> sinter k2 k3
1) "v4"
#sunion <key1><key2>返回两个集合的并集元素。
127.0.0.1:6379> sunion k2 k3
1) "v5"
2) "v3"
3) "v4"
4) "v2"
5) "v6"
6) "v7"
#sdiff <key1><key2>返回两个集合的差集元素(key1中的,不包含key2中的)
127.0.0.1:6379> sdiff k2 k3
1) "v2"
2) "v5"
3) "v3"

5、Hash哈希

Redis hash 是一个键值对集合。

Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储

#hset <key><field><value>给<key>集合中的  <field>键赋值<value>
127.0.0.1:6379> hset user:1001 id 1
(integer) 1
#hget <key1><field>从<key1>集合<field>取出 value 
127.0.0.1:6379> hget user:1001 id
"1"
#hmset <key1><field1><value1><field2><value2>... 批量设置hash的值
127.0.0.1:6379> hmset user:1002 id 2 name lisi age 18
OK
#hexists<key1><field>查看哈希表 key 中,给定域 field 是否存在。 
127.0.0.1:6379> hexists user1002 id
(integer) 0
#hkeys <key>列出该hash集合的所有field
127.0.0.1:6379> hkeys user:1002
1) "id"
2) "name"
3) "age"
#hincrby <key><field><increment>为哈希表 key 中的域 field 的值加上增量 1   -1
127.0.0.1:6379> hincrby user:1002 age 2
(integer) 20
#hsetnx <key><field><value>将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在
127.0.0.1:6379> hsetnx user:1002 gender 1
(integer) 1
#hvals <key>列出该hash集合的所有value
127.0.0.1:6379> hvals user:1002
1) "2"
2) "lisi"
3) "20"
4) "1"

6、Zset有序集合

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。

访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

#zadd  <key><score1><value1><score2><value2>… 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
127.0.0.1:6379> zadd topn 200 java 300 c++ 400 mysql 500 php
(integer) 4
#zrange <key><start><stop>  [WITHSCORES]   返回有序集 key 中,下标在<start><stop>之间的元素
127.0.0.1:6379> zrange topn 0 -1
1) "java"
2) "c++"
3) "mysql"
4) "php"
#带WITHSCORES,可以让分数一起和值返回到结果集。
127.0.0.1:6379> zrange topn 0 -1 withscores
1) "java"
2) "200"
3) "c++"
4) "300"
5) "mysql"
6) "400"
7) "php"
8) "500"
127.0.0.1:6379> zrangebyscore topn 300 500
1) "c++"
2) "mysql"
3) "php"
127.0.0.1:6379> zrangebyscore topn 300 500 withscores
1) "c++"
2) "300"
3) "mysql"
4) "400"
5) "php"
6) "500"
#zrevrangebyscore key maxmin [withscores] [limit offset count]  同上,改为从大到小排列。              
127.0.0.1:6379> zrevrangebyscore topn 500 200
1) "php"
2) "mysql"
3) "c++"
4) "java"
127.0.0.1:6379> zrevrangebyscore topn 500 200 withscores
1) "php"
2) "500"
3) "mysql"
4) "400"
5) "c++"
6) "300"
7) "java"
8) "200"
#zincrby <key><increment><value>      为元素的score加上增量
127.0.0.1:6379> zincrby topn 50 java
"250"
#zrem  <key><value>删除该集合下,指定值的元素 
127.0.0.1:6379> zrem topn php
(integer) 1
#zcount <key><min><max>统计该集合,分数区间内的元素个数 
127.0.0.1:6379> zcount topn 200 300
(integer) 2
#zrank <key><value>返回该值在集合中的排名,从0开始。
127.0.0.1:6379> zrank topn c++
(integer) 1

三、Redis配置文件介绍

自定义目录:/etc/redis.conf

Units单位

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

大小写不敏感

INCLUDES包含

类似jsp中的include,多实例的情况可以把公用的配置文件提取出来

网络相关配置

bind:

默认情况bind=127.0.0.1只能接受本机的访问请求

不写的情况下,无限制接受任何ip地址的访问

生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉

如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应

protected:将本机访问保护模式设置no

Port:端口号,默认 6379

tcp-backlog:

设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列。

在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。

注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128),所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果

timeout:一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。

tcp-keepalive:对访问客户端的一种心跳检测,每个n秒检测一次。单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60

GENERAL通用

daemonize:是否为后台进程,设置为yes;守护进程,后台启动

pidfile:存放pid文件的位置,每个实例会产生一个不同的pid文件

loglevel:指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice

logfile:日志文件名称

databases 16:设定库的数量 默认16,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id

SECURITY安全

在这里插入图片描述

访问密码的查看、设置和取消

在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。

永久设置,需要再配置文件中进行设置。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Af6QXoq-1659611338361)(.\redis-asset\image-20220728122908657.png)]

LIMITS限制

maxclients:

Ø 设置redis同时可以与多少个客户端进行连接。

Ø 默认情况下为10000个客户端。

Ø 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。

maxmemory:

Ø 建议必须设置,否则,将内存占满,造成服务器宕机

Ø 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。

Ø 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。

Ø 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。

maxmemory-policy:

Ø volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)

Ø allkeys-lru:在所有集合key中,使用LRU算法移除key

Ø volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键

Ø allkeys-random:在所有集合key中,移除随机的key

Ø volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key

Ø noeviction:不进行移除。针对写操作,只是返回错误信息

maxmemory-samples

Ø 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。

Ø 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。

四、 Redis的发布与订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

Redis 客户端可以订阅任意数量的频道。

发布订阅命令行实现

在A客户端订阅channel1

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

在客户端B给channel1发送消息

127.0.0.1:6379> publish channel1 hello
(integer) 1

打开客户端A接受消息

1) "message"
2) "channel1"
3) "hello"

五、Redis新数据类型

1、Bitmaps

简介:

合理地使用操作位能够有效地提高内存使用率和开发效率。

Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。

(2) Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hkoIkFtT-1659611338361)(.\redis-asset\image-20220728145055233.png)]

常见命令如下(示例):

#setbit<key><offset><value>设置Bitmaps中某个偏移量的值(0或1)
127.0.0.1:6379> setbit user:20210101 1 1
(integer) 0
127.0.0.1:6379> setbit user:20210101 6 1
(integer) 0
127.0.0.1:6379> setbit user:20210101 11 1
(integer) 0
127.0.0.1:6379> setbit user:20210101 15 1
(integer) 0
127.0.0.1:6379> setbit user:20210101 19 1
(integer) 0
#getbit<key><offset>获取Bitmaps中某个偏移量的值
127.0.0.1:6379> getbit user:20210101 8
(integer) 0
127.0.0.1:6379> getbit user:20210101 1
(integer) 1
#bitcount<key>[start end] 统计字符串从start字节到end字节比特值为1的数量
127.0.0.1:6379> bitcount user:20210101
(integer) 5
127.0.0.1:6379> bitcount user:20210101 1 3
(integer) 3
127.0.0.1:6379> setbit user:20210103 0 1
(integer) 0
127.0.0.1:6379> setbit user:20210103 1 1
(integer) 0
127.0.0.1:6379> setbit user:20210103 4 1
(integer) 0
127.0.0.1:6379> setbit user:20210103 9 1
(integer) 0
127.0.0.1:6379> bitcount user:20210103
(integer) 4
#bitop  and(or/not/xor) <destkey> [key…]
#bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
127.0.0.1:6379> bitop and user:20210101 user:20200103
(integer) 0
127.0.0.1:6379> bitop and user:20210101 user:20210103
(integer) 2
127.0.0.1:6379> bitop or user:20210101 user:20210103
(integer) 2

总结:在实际的开发中我们可以用Bitmaps记录独立用户是否有访问过该网站

2、HyperLogLog

简介:

HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

常见命令如下(示例):

#pfadd <key>< element> [element ...]   添加指定元素到 HyperLogLog 中
#将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0。
...
127.0.0.1:6379> pfadd program "java"
(integer) 0
127.0.0.1:6379> pfadd program "c++" "mysql"
(integer) 1
#pfcount<key> [key ...] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
127.0.0.1:6379> pfcount program
(integer) 4
127.0.0.1:6379> pfadd k1 "a"
(integer) 1
127.0.0.1:6379> pfadd k1 "b"
(integer) 1
127.0.0.1:6379> pfcount k1
(integer) 2
#pfmerge<destkey><sourcekey> [sourcekey ...]  将一个或多个HLL合并后的结果存储在另一个HLL中
127.0.0.1:6379> pfmerge k100 k1 program
OK
127.0.0.1:6379> pfcount k100
(integer) 6

总结:在实际需求中,可能需要统计网站PV(PageView页面访问量)UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题

3、Geospatial

简介:

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

常见命令如下(示例):

#geoadd<key>< longitude><latitude><member> [longitude latitude member...]   添加地理位置(经度,纬度,名称)
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
#两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。
#有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。
#当坐标位置超出指定范围时,该命令将会返回一个错误。
#已经添加的数据,是无法再次往里面添加的。
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.90 beijing
(integer) 3
#geopos  <key><member> [member...]  获得指定地区的坐标值
127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47000163793563843"
   2) "31.22999903975783553"
#geodist<key><member1><member2>  [m|km|ft|mi ]  获取两个位置之间的直线距离
#m 表示单位为米[默认值]。
#km 表示单位为千米。
#mi 表示单位为英里。
#ft 表示单位为英尺。
#如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位
127.0.0.1:6379> geodist china:city beijing shanghai km
"1068.1535"
#georadius<key>< longitude><latitude>radius  m|km|ft|mi   以给定的经纬度为中心,找出某一半径内的元素
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "chongqing"
2) "shenzhen"

六、Jedis_测试

1、导入相关依赖

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

2、Jedis基本操作

2.1 Key

jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.set("k3", "v3");
Set<String> keys = jedis.keys("*");
System.out.println(keys.size());
for (String key : keys) {
System.out.println(key);
}
System.out.println(jedis.exists("k1"));
System.out.println(jedis.ttl("k1"));                
System.out.println(jedis.get("k1"));

2.2 String

jedis.mset("str1","v1","str2","v2","str3","v3");
System.out.println(jedis.mget("str1","str2","str3"));

2.3 List

List<String> list = jedis.lrange("mylist",0,-1);
for (String element : list) {
System.out.println(element);
}

2.4 Set

jedis.sadd("orders", "order01");
jedis.sadd("orders", "order02");
jedis.sadd("orders", "order03");
jedis.sadd("orders", "order04");
Set<String> smembers = jedis.smembers("orders");
for (String order : smembers) {
System.out.println(order);
}
jedis.srem("orders", "order02");

2.5 hash

jedis.hset("hash1","userName","lisi");
System.out.println(jedis.hget("hash1","userName"));
Map<String,String> map = new HashMap<String,String>();
map.put("telphone","13810169999");
map.put("address","atguigu");
map.put("email","abc@163.com");
jedis.hmset("hash2",map);
List<String> result = jedis.hmget("hash2", "telphone","email");
for (String element : result) {
System.out.println(element);
}

2.6 zset

jedis.zadd("zset01", 100d, "z3");
jedis.zadd("zset01", 90d, "l4");
jedis.zadd("zset01", 80d, "w5");
jedis.zadd("zset01", 70d, "z6");
 
Set<String> zrange = jedis.zrange("zset01", 0, -1);
for (String e : zrange) {
System.out.println(e);
}

3、注意事项

禁用Linux的防火墙:Linux(CentOS7)里执行命令

systemctl stop/disable firewalld.service

redis.conf中注释掉bind 127.0.0.1 ,然后 protected-mode no

七、SpringBoot整合redis

1、导入相关依赖

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

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

2、在.properties添加redis配置

#Redis服务器地址
spring.redis.host=192.168.140.136
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
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

3、添加redis配置类

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer 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);
//key序列化方式
        template.setKeySerializer(redisSerializer);
//value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer 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);
// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

4、测试Redis

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping
    public String testRedis() {
        //设置值到redis
        redisTemplate.opsForValue().set("name","lucy");
        //从redis获取值
        String name = (String)redisTemplate.opsForValue().get("name");
        return name;
    }
}

八、Redis_事务

前言

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis事务的主要作用就是串联多个命令防止别的命令插队。

1、事务的基本操作

从输入multi开始执行事务,输入命令后会依次进入队列,直到输入exec执行事务后,Redis才会将队列中的命令依次执行。组队阶段可以用过discard放弃组队。

在这里插入图片描述

正常执行事务:

127.0.0.1:6379> multi#开启事务
OK
127.0.0.1:6379(TX)> set v1 v11  #添加数据
QUEUED
127.0.0.1:6379(TX)> set v2 v22  #添加数据
QUEUED
127.0.0.1:6379(TX)> set v3 v33  #添加数据
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) OK
127.0.0.1:6379> get v1
"v11"

放弃事务:

127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379(TX)> set v1 v11 #添加数据
QUEUED
127.0.0.1:6379(TX)> set v2 v22
QUEUED
127.0.0.1:6379(TX)> discard #放弃事务
OK
127.0.0.1:6379> get v11 #不会执行事务里面的添加操作、、
(nil)

2、事务的错误处理

组队阶段某个命令出现错误,执行事务时整个队列会被取消
在这里插入图片描述

代码如下(示例):

127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set b1 b11 #添加数据
QUEUED
127.0.0.1:6379(TX)> set b2 b22
QUEUED
127.0.0.1:6379(TX)> set b3 #输入一个错误的命令,这时候已经报错了,但是这个还是进入了事务的队列当中
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set b4 b44 
QUEUED
127.0.0.1:6379(TX)> exec #执行事务,报错,并且所有的命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get b1 #获取数据为空,证明没有执行
(nil)

执行阶段某个命令发生错误,只用错误命令不执行,其他命令照常执行且不回滚

在这里插入图片描述

代码如下(示例):

127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set n1 n11 #添加字符串数据
QUEUED
127.0.0.1:6379(TX)> incr n1 #对字符串数据进行自增操作
QUEUED
127.0.0.1:6379(TX)> set n2 n22
QUEUED
127.0.0.1:6379(TX)> exec #执行事务。虽然对字符串数据进行自增操作报错了,但是其他的命令还是可以正常执行的
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get n1 #获取数据失败
"n11"
127.0.0.1:6379> get n2 #获取数据成功
"n22"

3、事务冲突的问题

悲观锁

悲观锁顾名思义就是很悲观,每当被拿数据时都会上锁。在当前步骤没有完成前,不让任何线程执行,十分浪费性能,一般不适用。在传统关系型数据库用到很多这种锁机制,不如行锁、表锁等,读锁、写锁等都是在操作前上锁。

在这里插入图片描述

乐观锁

乐观锁顾名思义就是很乐观,每个拿数据时都会认为别人不认修改,所以不上锁。但是每当数据更新时需要判断是否被其他人修改数据,类似添加版本号,当版本号发生改变事务执行失败。乐观锁适用于多读的应用类型,这样可以提高吞吐量

在这里插入图片描述

watch(监视)

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

代码如下(示例):

#线程1
127.0.0.1:6379> set balance 100 #添加数据
OK
127.0.0.1:6379> watch balance #开启监视(乐观锁)
OK
127.0.0.1:6379> multi #开始事务
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec #执行事务后版本号发生改变
1) (integer) 110
#线程2
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 20
QUEUED
127.0.0.1:6379(TX)> exec #版本号和之前不同,事务执行失败
(nil)

4、Redis事务的三大特性

单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

九、Redis的持久化

前言

Redis是内存数据库本身运行时数据保存在内存中,为了防止非正常原因宕机或者关闭redis的进程或者关闭计算机后数据被会操作系统清除,Redis提供两种不同形式的持久化方式

  • RDB(Redis DataBase)
  • AOF(Append Of File)

RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,默认写入文件dump.rdb,它恢复时是将快照文件直接读到内存里

redis.conf的配置

redis.conf中配置文件名称,默认为dump.rdb

在这里插入图片描述

rdb文件的默认路径,也可以修改

在这里插入图片描述

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

在这里插入图片描述

命令 save VS bgsave

save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。

bgsaveRedis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。

可以通过lastsave 命令获取最后一次成功执行快照的时间

触发RDB快照

将*.rdb的文件拷贝到别的地方

rdb的恢复:

  • 关闭Redis
  • 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  • 启动Redis, 备份数据会直接加载

优缺点

优点:

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

缺点:

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

总结

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

在这里插入图片描述


AOF

日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

redis.conf的配置

AOF默认不启动,保存路径同RDB一致

在这里插入图片描述

AOF同步频率设置

在这里插入图片描述

appendfsync always

始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

appendfsync everysec

每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

appendfsync no

redis不主动进行同步,把同步时机交给操作系统。

AOF启动/修复/恢复

AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

异常恢复:

  • 修改默认的appendonly no,改为yes
  • 如遇到AOF文件损坏**,通过/usr/local/bin/redis-check-aof–fix appendonly.aof进行恢复
  • 备份被写坏的AOF文件
  • 恢复:重启redis,然后重新加载

优缺点

优点:

  • 备份机制更稳健,丢失数据概率更低。
  • 可读的日志文本,通过操作AOF稳健,可以处理误操作。

缺点:

  • 比起RDB占用更多的磁盘空间。
  • 恢复备份速度要慢。
  • 每次读写都同步的话,有一定的性能压力。
  • 存在个别Bug,造成恢复不能。

总结

在这里插入图片描述

AOF和RDB比较

同时启动redis听谁的?

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

用哪个好?

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

十、主从复制

前言

在大量用户访问服务器时,为了减小redis服务器访问的压力,可以使用主从复制为服务器分担压力

1、概念

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

  • 读写分离,性能扩展(主机写,从机读)
  • 容灾快速恢复(当一台从机挂了,还可以读取其他从机)

2、环境配置

开启三台服务:

[root@instance-dsmx2a9u myredis]# cp /etc/redis.conf /myredis/redis.conf #复制redis配置文件
[root@instance-dsmx2a9u myredis]# sudo nano redis6379.conf #编辑第一个配置文件(如下)
[root@instance-dsmx2a9u myredis]# cp redis6379.conf redis6380.conf #再复制两个配置文件
[root@instance-dsmx2a9u myredis]# cp redis6379.conf redis6381.conf
[root@instance-dsmx2a9u myredis]# redis-server redis6379.conf #启动三台服务器
..
[root@instance-dsmx2a9u myredis]# ps -ef | grep redis
root     28069     1  0 16:14 ?        00:00:00 redis-server *:6379
root     28079     1  0 16:14 ?        00:00:00 redis-server *:6380
root     28087     1  0 16:14 ?        00:00:00 redis-server *:6381
root     28095 27238  0 16:15 pts/0    00:00:00 grep --color=auto redis

新建配置文件填写一下内容:

include /myredis/redis.conf #拷贝多个redis.conf文件include(写绝对路径)
pidfile /var/run/redis_6379.pid #Pid文件名字pidfile
port 6379 #指定端口port	
dbfilename dump6379.rdb #dump.rdb名字dbfilename

slaveof :成为某个实例的从服务器(将80和81设置为从服务器)

在这里插入图片描述

info replication:打印主从复制的相关信息(查看当前服务器的主从关系)

在这里插入图片描述

3、常用三招

一主二扑

当连接上主服务器之后,从服务器向主服务器发送进行数据同步消息

主服务器连接从服务器的消息,把主服务器数据进行持久化,rdb文件,把rdb文件发送给从服务器过去rdb文件后同步数据

每次主服务器进行写操作时,从服务器都不重新服务数据

在这里插入图片描述

薪火相传

在这里插入图片描述

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

用 slaveof

中途变更转向:会清除之前的数据,重新建立拷贝最新的

风险是一旦某个slave宕机,后面的slave都没法备份

主机挂了,从机还是从机,无法写数据了

反客为主

当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。

slaveof no one 将从机变为主机。

4、哨兵模式

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

环境配置

新建sentinel.conf文件

#其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。 
sentinel monitor mymaster 127.0.0.1 6379 1

启动哨兵

[root@instance-dsmx2a9u myredis]# redis-sentinel  /myredis/sentinel.conf 

在这里插入图片描述

复制延时

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

故障恢复

在这里插入图片描述

优先级在redis.conf中默认:slave-priority 100,值越小优先级越高

偏移量是指获得原主机数据最全的

每个redis实例启动后都会随机生成一个40位的runid

主从复制

private static JedisSentinelPool jedisSentinelPool=null;

public static  Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
            Set<String> sentinelSet=new HashSet<>();
            sentinelSet.add("192.168.11.103:26379");

            JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong

jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
        }else{
return jedisSentinelPool.getResource();
        }
}

十一、应用问题解决

1、缓存穿透

1.1、概念

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

1.2、解决方法

(1) **对空值缓存:**如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟

(2) 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)

将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

(4) **进行实时监控:**当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

2、缓存击穿

1、概念

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

2、解决方法

(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长

(3)使用锁:

  1. 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
  2. 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
  3. 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
  4. 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

3、缓存雪崩

3.1 概念

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key正常访问

在这里插入图片描述

3.2 解决方法

(1) 构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)

(2) 使用锁或队列:

用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况

(3) 设置过期标志更新缓存:

记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。

(4) 将缓存失效时间分散开:

比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

十二、分布式锁

前言

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

1、基本原理和实现方式对比

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

在这里插入图片描述

分布式锁主流的实现方案有哪些?

基于数据库实现分布式锁:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见

基于缓存(Redis等):redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁

基于Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案

在这里插入图片描述

2、Redis分布式锁的实现核心思路

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回true,失败返回false
  • 释放锁:
    • 手动释放
    • 超时释放:获取锁时添加一个超时时间

核心思路:

我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8gdCCczc-1659611338370)(.\redis-asset\image-20220803150505532.png)]

3、解决Redis分布式锁误删问题

逻辑说明:

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。

在这里插入图片描述

**核心逻辑:**在存入锁事时,放入自己线程的标识(UUID,在删除锁时,判断当前这把锁的标识是不是自己存入的
在这里插入图片描述

具体代码如下:加锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
   // 获取线程标示
   String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}

释放锁

public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

有关代码实操说明:

在我们修改完此处代码后,我们重启工程,然后启动两个线程,第一个线程持有锁后,手动释放锁,第二个线程 此时进入到锁内部,再放行第一个线程,此时第一个线程由于锁的value值并非是自己,所以不能释放锁,也就无法删除别人的锁,此时第二个线程能够正确释放锁,通过这个案例初步说明我们解决了锁误删的问题。

4、Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性(当前命令在执行时其他命令不能干预)。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html,这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了,

具体代码如下:

@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);

总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

- 互斥性。在任意时刻,只有一个客户端能持有锁。

- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

- 加锁和解锁必须具有原子性。

常见代码如下:加锁

// 1. 从redis中获取锁,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
      .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);

使用lua释放锁

// 2. 释放锁 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置lua脚本返回类型为Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

重试

Thread.sleep(500);
testLock();
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值