Redis总结

Redis总结

Redis是一种Key-value数据库

Redis是nosql(非关系型数据库)技术阵营的一员,可以胜任如缓存、队列系统的不同角色。

Redis特性

Redis与其他key-value缓存产品有以下三个特点:

  • Redis支持数据库持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。

  • Redis不仅仅支持简单的key-value类型数据,同时还提供String、 list、set、zset、hash等数据结构的存储。

  • Redis支持数据备份,即master-slave(主从)模式的数据备份。

Redis优势

  • 性能极高:读取速度快!!! 读110000次/s,写81000次/s。差不多是mysql读写速度的8~20倍。
  • 丰富的数据类型:支持 String、Lists、Sets、Hashes、Ordered Sets等操作。
  • 原子:Redis的所有操作都是原子性的,同时支持几个操作全并后的原子性执行。
  • 丰富的特性:支持publish/subscribe,通知,key过期等。

Redis应用场景

  • 做缓存:redis的所有数据是放在内存中的(内存数据库)。
  • 在特定场景下代替传统数据库:如社交类应用
  • 大型系统中,实现特定功能如session共享购物车

性能测试

redis-benchmark:是一个压力测试工具!

#测试:100个并发连接,100000个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

在这里插入图片描述

Redis基础知识:

redis默认有16个数据库

(默认使用的是第0个数据库)可以使用select进行切换数据库

在这里插入图片描述

查看数据库大小&清空数据库
#查看数据库大小
dbsize
#清空数据库 
flushdb
#查看该库内有多少个key
keys *

Redis是单线程的!

明白Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!所以就使用了单线程了!

Redis为什么单线程还这么快?

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速;
  • 单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 高效的数据结构;操作也简单
  • 采用了非阻塞I/O多路复用机制;

1、误区1:高性能的服务器一定是多线程的?
2、误区2∶多线程(CPU上下文会切换!)一定比单线程效率高!先去CPU>内存>硬盘的速度要有所了解!
核心: Redis,是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换︰耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!

在这里插入图片描述

Redis官网有命令API,忘记可以查阅

Redis-Key相关的操作

#查看该库内有多少个key
keys *
#判断某个key是否存在
exists [keyname]
#移除key, 1代表当前数据库
move [keyname] 1
#设置过期时间,单位是秒
expire [keyname] [时间]
expire myname 10 #myname 10秒过期
#查看当前key的剩余时间
ttl [keyname]
#查看当前key的类型
type [keyname]

5种基本数据类型

String:缓存、计数器、分布式锁等。
List:链表、队列、微博关注人时间轴列表等。
Hash:用户信息、Hash 表等。
Set:去重、赞、踩、共同好友等。
Zset:访问量排行榜、点击量排行榜等。

String(字符串)
#设置值
127.0.0.1:6379> set key1 hello
#给value追加值。如果当前key不存在就相当于set操作
127.0.0.1:6379> append key1 world
#获取值
127.0.0.1:6379> get key1
"helloworld"
#获取字符串长度
127.0.0.1:6379> strlen key1
#截取字串
127.0.0.1:6379> getrange key1 0 3
"hell"
#替换指定位置的值
127.0.0.1:6379> setrange key1 3 ,
(integer) 10
127.0.0.1:6379> get key1
"hel,oworld"

自增自减操作

127.0.0.1:6379> set key2 0
#让值自增1
127.0.0.1:6379> incr key2
(integer) 1
127.0.0.1:6379> incr key2
(integer) 2
127.0.0.1:6379> incr key2
(integer) 3
127.0.0.1:6379> get key2
"3"
#让值自减1
127.0.0.1:6379> decr key2
(integer) 2
127.0.0.1:6379> decr key2
(integer) 1
127.0.0.1:6379> get key2
"1"
#设置步长加固定步长值
127.0.0.1:6379> incrby key2 10
(integer) 11
127.0.0.1:6379> get key2
"11"
#让值减固定值
127.0.0.1:6379> decrby key2 5
(integer) 6
127.0.0.1:6379> get key2
"6"

setex和setnx:

#setex (set with expire) #设置过期时间
#setnx (set if no exist) #key不存在再设置(再分布式锁中会常常使用!)

127.0.0.1:6379> setex key3 10 "hello" #设置key3  10秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 4
127.0.0.1:6379> get key3 #10秒后key3过期了
(nil)
127.0.0.1:6379> setnx key4 "redis"
(integer) 1
127.0.0.1:6379> keys *
1) "key1"
2) "key4"
3) "key2"
127.0.0.1:6379> setnx key4 "MongoDB" #试图设置key4,但不会成功!
(integer) 0
127.0.0.1:6379> get key4 #key4值未变
"redis"

批量操作:

#设置多个值
127.0.0.1:6379> mset key1 aaa key2 bbb key3 ccc
OK
127.0.0.1:6379> keys *
1) "key3"
2) "key1"
3) "key2"
#获取多个值
127.0.0.1:6379> mget key1 key2 key3
1) "aaa"
2) "bbb"
3) "ccc"

# key4也没成功,可见msetnx是一个原子性操作,要么一起成功,要么一起失败!
127.0.0.1:6379> msetnx key1 "AAA" key4 ddd 
(integer) 0
127.0.0.1:6379> keys *
1) "key3"
2) "key1"
3) "key2"

设置对象:

设置一个user:1对象,值为json字符串来保存一个对象:set user:1{name:zhangsna,age:22}

user:{id}:{filed} 如此设计在redis中是完全ok了!

127.0.0.1:6379> mset user:1:name zhangsan user:1:age 22
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "22"

getset操作:先get再set

127.0.0.1:6379> getset mykey china  #因为此时没有mykey,所以返回是空
(nil)
127.0.0.1:6379> get mykey
"china"
127.0.0.1:6379> getset mykey chinese #先获取当前mykey的值,再设置mykey
"china"
127.0.0.1:6379> get mykey
"chinese"

String类似的使用场景:value除了是我们的字符串外,还可以是数字!

  • 计数器
  • 统计数量
  • 粉丝数
  • 对象缓存存储等
List(列表)

在redis里,我们可以把list用作栈,队列,阻塞队列

所有的list命令都是以l开头的

#从左侧(头部)放入元素
127.0.0.1:6379> lpush list1  one 
127.0.0.1:6379> lpush list1 tow
127.0.0.1:6379> lpush list1 three
#取出所有元素
127.0.0.1:6379> lrange list1 0 -1
1) "three"
2) "tow"
3) "one"
#有没有发现什么不同,我们存入顺序是one,tow,three 取出的顺序是three,tow,one

#从右侧(尾部)放入元素
127.0.0.1:6379> rpush list1 four
(integer) 4
127.0.0.1:6379> lrange list1 0 -1
1) "three"
2) "tow"
3) "one"
4) "four"

#######################################################################
#移除元素:LPOP、RPOP

#从左侧移除
127.0.0.1:6379> lpop list1
"three"

#从右侧移除
127.0.0.1:6379> rpop list1
"four"
127.0.0.1:6379> lrange list1 0 -1
1) "tow"
2) "one"

#######################################################################
#还可以利用list的index下标取值
127.0.0.1:6379> lrange list1 0 -1
1) "tow"
2) "one"
127.0.0.1:6379> lindex list1 1
"one"
127.0.0.1:6379> lindex list1 0
"tow"

#######################################################################
#移除指定个数的值
127.0.0.1:6379> lpush list1 three
(integer) 4
127.0.0.1:6379> lrange list1 0 -1  #现在有两个three
1) "three"
2) "three"
3) "tow"
4) "one"
127.0.0.1:6379> lrem list1 2 three #移除list1中的2个three
(integer) 2
127.0.0.1:6379> lrange list1 0 -1
1) "tow"
2) "one" 

#######################################################################
# trim 截断 list
127.0.0.1:6379> lrange list1 0 -1
1) "five"
2) "four"
3) "three"
4) "tow"
5) "one"
127.0.0.1:6379> ltrim list1 1 3  #截取第二个和第四个
OK
127.0.0.1:6379> lrange list1 0 -1
1) "four"
2) "three"
3) "tow"

#######################################################################
# rpoplpush 移除列表的最后一个元素并push到新的列表中

127.0.0.1:6379> lrange list1 0 -1
1) "four"
2) "three"
3) "tow"
127.0.0.1:6379> rpoplpush list1 list1  #从右侧移除又从左侧添加回来了
"tow"
127.0.0.1:6379> lrange list1 0 -1
1) "tow"
2) "four"
3) "three"
127.0.0.1:6379> rpoplpush list1 list2  #从右侧移除并从左侧添加到一个新list中
"three"
127.0.0.1:6379> lrange list2 0 -1
1) "three"

##################################更新相关的操作#####################################
# lset 重新设置某个
127.0.0.1:6379> exists list  #判断是否存在当前key,可见不存在当前list
(integer) 0
127.0.0.1:6379> lset list 0 aaa #如果对不存在key去更新就会报错
(error) ERR no such key
127.0.0.1:6379> lpush list aaa  #创建list添加元素
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "aaa"
127.0.0.1:6379> lset list 0 1111 #更新list的第0个元素
OK
127.0.0.1:6379> lrange list 0 -1 #更新成功!
1) "1111"
127.0.0.1:6379> lset list 1 2222 #当前list只有一个元素,若更新第二个位置就会报错
(error) ERR index out of range


# linsert 在某一元素的前面或后面插入一个值
127.0.0.1:6379> lrange list1 0 -1
1) "tow"
2) "four"
127.0.0.1:6379> linsert list1 before "tow" "one"
(integer) 3
127.0.0.1:6379> linsert list1 after "tow" "three"
(integer) 4
127.0.0.1:6379> lrange list1 0 -1
1) "one"
2) "tow"
3) "three"
4) "four"

小结:list其实是个链表。场景:消息队列,栈!

Set(集合)

set的性质:无序不可重复,因此set不能添加重复的值。

set的所有操作都是以s开头。

添加元素&删除元素

127.0.0.1:6379> sadd set1 zhangsan  #往set1中添加元素
(integer) 1
127.0.0.1:6379> sadd set1 lisi
(integer) 1
127.0.0.1:6379> sadd set1 wanger
(integer) 1
127.0.0.1:6379> smembers set1  #查看set1中的所有元素
1) "wanger"
2) "zhangsan"
3) "lisi"
127.0.0.1:6379> sismember set1 hello #判断是否有hello成员,可见没有
(integer) 0
127.0.0.1:6379> sismember set1 zhangsan 
(integer) 1
127.0.0.1:6379> scard set1 #获取set中成员个数
(integer) 3
127.0.0.1:6379> srem set1 wanger #移除指定元素
(integer) 1
127.0.0.1:6379> scard set1
(integer) 2
127.0.0.1:6379> smembers set1
1) "zhangsan"
2) "lisi"
######################################################################
# 随机抽选元素
127.0.0.1:6379> smembers set1 #查看所有元素
1) "zhangsan"
2) "lisi"
127.0.0.1:6379> srandmember set1 #不指定个数,默认随机抽取一个元素
"zhangsan"
127.0.0.1:6379> srandmember set1 1 #随机抽取一个元素
1) "lisi"
127.0.0.1:6379> srandmember set1 2 #随机抽取两个元素
1) "zhangsan"
2) "lisi"
127.0.0.1:6379> srandmember set1 1 #随机抽取一个元素
1) "zhangsan"
#######################################################################
随机删除元素

127.0.0.1:6379> smembers set1 #查看元素个数
1) "zhangsan"
2) "lisi"
127.0.0.1:6379> spop set1 1 #随机删除一个元素
1) "zhangsan"
127.0.0.1:6379> spop set1 #不指定个数,默认随机删除一个元素
"lisi"
127.0.0.1:6379> smembers set1 #查看元素个数,空了
(empty list or set)

#######################################################################
将一个指定值,移动到另外一个set集合

127.0.0.1:6379> smembers set1
(empty list or set)
127.0.0.1:6379> sadd set2 zhaoliu
(integer) 1
127.0.0.1:6379> smembers set2
1) "zhaoliu"
127.0.0.1:6379> sadd set2 liuyifei
(integer) 1
127.0.0.1:6379> sadd set2 linyuner
(integer) 1
127.0.0.1:6379> sadd set2 xulingyue
(integer) 1
127.0.0.1:6379> smove set2 set1 "xulingyue"  #将set2中的元素移动到set1 最后并指定移动哪个元素。
(integer) 1
127.0.0.1:6379> smembers set1  #set1中有了xulingyue
1) "xulingyue"
127.0.0.1:6379> smembers set2 #set2中xulingyue没有了
1) "liuyifei"
2) "linyuner"
3) "zhaoliu"
#######################################################################
微博,b站,共同关注!(并集)
交集
差集
并集
127.0.0.1:6379> sadd set1 a
(integer) 1
127.0.0.1:6379> sadd set1 b
(integer) 1
127.0.0.1:6379> sadd set1 c
(integer) 1
127.0.0.1:6379> sadd set2 c
(integer) 1
127.0.0.1:6379> sadd set2 d
(integer) 1
127.0.0.1:6379> sadd set2 e
(integer) 1
127.0.0.1:6379> sdiff set1 set2 #差集
1) "a"
2) "b"
127.0.0.1:6379> sinter set1 set2 #交集
1) "c"
127.0.0.1:6379> sunion set1 set2 #并集
1) "a"
2) "b"
3) "c"
4) "e"
5) "d"

微博,将A用户所有关注的人放在一个set集合中!将他的粉丝也放在一个集合中!

共同关注,共同爱好,二度好友,推荐好友!(六度分隔理论):世界上任何两个陌生人都可以通过6个人建立联系。

Hash(散列表)

想象成Map集合!key-value →(只不过这个value值是一个Map)key-map → key-

#设置
127.0.0.1:6379> hset myhash field1 zhangsan
(integer) 1
#获取
127.0.0.1:6379> hget myhash field1
"zhangsan"

#设置多值
127.0.0.1:6379> hmset myhash field1 lisi field2 wangwu 
OK

#获取多值
127.0.0.1:6379> hmget myhash field1 field2
1) "lisi"
2) "wangwu"

#获取所有值
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "lisi"
3) "field2"
4) "wangwu"

#删除值
127.0.0.1:6379> hdel myhash field1
(integer) 1

#判断存在
127.0.0.1:6379> hexists myhash field1
(integer) 0

#获得所有key
127.0.0.1:6379> hkeys myhash
1) "field2"

#获得所有value
127.0.0.1:6379> hvals myhash
1) "wangwu"

127.0.0.1:6379> hset myhash field1 5
(integer) 1

#加1
127.0.0.1:6379> hincrby myhash field1 1
(integer) 6

#减1
127.0.0.1:6379> hincrby myhash field1 -1
(integer) 5

127.0.0.1:6379> hsetnx myhash field3 hello #如果不存在可以设置成功!
(integer) 1
127.0.0.1:6379> hsetnx myhash field3 world #如果存在则不能设置
(integer) 0

Zset(sorted set有序集合)
127.0.0.1:6379> zadd salary 2500 xiaohong  # 添加三个用户
(integer) 1
127.0.0.1:6379> zadd sa1ary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 kaungshen
(integer) 1

# ZRANGEBYSCORE KEY min max
127.0.0.1:6379>ZRANGESYSCORE salary -inf +inf # 显示全部的用户,自动排序从小到大!
1)"kaungshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379>ZREVRANGE salary 0 -1 # 显示全部的用户,自动排序从大到小!

127.0.0.1:6379>ZRANGEBYSCORE salary -inf +inf withscores # 显示全部的用户并且附带成绩
1) "kaungshen"
2) "500"
3)"xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores #显示工资小于2500员工的升序排序!
1)"kaungshen"
2) "500"
3) "xiaohong"
4)"2500"
#############################################################################
#移除元素
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary xiaohong #移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "kaungshen"
2) "zhangsan"
127.0.0.1:6379> zcard salary#获取有序集合中的个数
(integer) 2
##########################################################################127.0.0.1:6379>zadd myset 1 he1lo
(integer) 1
127.0.0.1:6379> zadd myset 2 wor1d 3 kuangshen
(integer) 2
127.0.0.1:6379> zcount myset 1 3 #获取指定区间的成员数量!
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2

班级表,工资表,排行榜场景等等。

3种特殊数据类型

Geospatial(地理位置)

简称Geo

朋友定位,附近范围的人,两地之间距离计算等

#添加
#geoadd 城市 纬度 经度

#getadd 添加地理位置
#规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
#参数key值()
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer)1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqi 114.05 22.52 shengzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2

############################################################################
#获取 获得当前定位:一定是一个坐标值!
127.0.0.1:6379>GEOPOS china:city beijing # 获取指定的城市的经度和纬度!
1)1)"116.39999896287918091"
  2) "39.9o000009167092543"
127.0.0.1:6379>GEOPOS china:city beijing chongqi
1) 1) "116.39999896287918091"
   2)"39.90000009167092543"
2)1) "106.49999767541885376"
  2) "29.52999957900659211"
  #############################################################################
#两地之间直线距离
两人之间的距离!单位︰
. m表示单位为米。
. km表示单位为千米。
. mi表示单位为英里。
. ft表示单位为英尺。
127.0.0.1:6379>GEODIST china:city beijing shanghai km #查看上海到北京的直线距离
"1067.3788"
127.0.0.1:6379> GEODIST china:city beijing chongqi km # 查看重庆到北京的直线距离
"1464.0708"
########################################################################################

#我附近的人?(获得所有附近的人的地址,定位!)
#通过半径来查询!获得指定数量的人,200
#所有数据应该都录入: china:city ,才会让结果更加请求!

127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 以110,30这个经纬度为中心,寻找方圆1000km内的城市。
1) "chongqi"
2) "xian"
3)"shengzhen "
4) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km
1)"chongqi"
2) "xian"
127.0.0.1:6379>GEORADIUS china:city 110 30 500 km withdist # 显示到中间距离的位置
1)1) "chongqi"
  2) "341.9374"
2)1) "xian"
  2) "483.8340"

127.0.0.1:6379>GEORADIUs china:city 110 30 500 km withcoord #显示他人的定位信息
1)1) "chongqi"
  2) 1) "106.49999767541885376"
     2) "29.52999957900659211"
2) 1) "xian"
   2) 1) "108.96000176668167114"
      2) "34.25999964418929977"

127.0.0.1:6379> GEORADTUS china:city 110 30 500 km withdist withcoord count 1 #筛选出指定数量的结果!
1)1) "chongqi"
  2) "341.9374"
  3) 1) "106.49999767541885376"
     2) "29.52999957900659211"
     
##############################################################################

#找出位于指定元素周围的其他元素!
127.0.0.1:6379>GEORADIUSBYMEMBER china:city beijing 1000 km
1) "beijing"
2) "xian"
127.0.0.1:6379>GEORADIUSBYMEMBER china:city shanghai 400 km
1) "hangzhou"
2) "shanghai"

#################################################################################

#GEOHASH命令-返回一个或多个位置元素的Geohash表示。该命令将返回11个字符的Geohash字符串!
# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近!
127.0.0.1:6379> geohash china:city beijing chongqing
1) "wx4fbxxfkeo"
2) "wm5xzrvbtvo" 

#########################################################################
#GEO底层的实现原理其实就是Zset !我们可以使用Zset命令来操作geo !
127.0.0.1:6379>ZRANGE chira:city 0 -1 # 查看地图中全部的元素
1) "chongqi"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing #移除指定元素!
(integer) 1
127.0.0.1:6379>ZRANGE china:city 0 -1
1) "chongqi"
2) "xian"
3) "shengzhen"
4) "hangzhou"
5) "shanghai"
Hyperloglog(基数统计)

什么是基数?
A{1,3,5,7,8,7}

B{1,3,5,7,8}
基数(不重复的元素)=5,可以接受误差!

Redis 2.8.9版本就更新了Hyperloglog数据结构!

Redis Hyperloglog基数统计的算法!
优点∶占用的内存是固定,2^64不同的元素的技术,只需要废12KB内存!如果要从内存角度来比较的话Hyperloglog首选!

网页的UV(一个人访问一个网站多次,但是还是算作一个人! )
传统的方式,set保存用户的id,然后就可以统计set 中的元素数量作为标准判断!
这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id ;

0.81%错误率!统计UV任务,可以忽略不计的!

127.0.0.1:6379> PFadd mykey a b c d e f g h i j# 创建第一组元素 mykey
(integer) 1
127.0.0.1:6379> PFCOUNT mykey# 统计mykey元素的基数数量
(integer) 10
127.0.0.1:6379>PFadd mykey2 i j z x c v b n m #创建第二组元素 mykey2
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 9
127.0.0.1:6379>PFMERGE mykey3 mykey mykey2 #合并两组 mykey mykey2 => mykey3并集
oK
127.0.0.1:6379>PFCOUNT mykey3#看并集的数量!
(integer) 15
Bitmaps(位图)

统计用户信息,用 0 1 表示两种状态:

活跃,不活跃!

登录,未登录!

打卡,未打卡!

Bitmaps位图,数据结构!都是操作二进制位来进行记录,就只有0 和1 两个状态!

365 天 = 365 bit ; 1字节 = 8bit ; 需要46个字节左右即可;

#使用bitmap来记录周一到周日的打卡!周一∶1 周二:0 周三:0 周四∶1....
127.0.0.1:6379> setbit sign 0 1
(integer)0
127.0.0.1:6379> setbit sign 1 0
(integer)0
127.0.0.1:6379> setbit sign 2 0
(integer)0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer)0
127.0.0.1:6379> setbit sign 5 0
(integer)0
127.0.0.1:6379> setbit sign 6 0
(integer)0
#############################################################################

#查看某一天是否有打卡!
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 6
(integer) 0

#统计操作,统计打卡的天数!
127.0.0.1:6379> bitcount sign # 统计这周的打卡记录,就可以看到是否有全勤!
(integer) 3

Redis事务:(multi & exec)

首先明白,事务的本质:要么全执行,要么全不执行!

Redis单条命令是保证原子性的,但是Redis事务不保证原子性!

Redis事务的本质:一组命令的集合!

这组命令要么全执行,要么全不执行。

一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行!

一次性、顺序性、排他性地执行这一系列的命令!

------ 队列 set..  set..  set..  执行 ------

Redis事务没有没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行! 只有发起执行命令的时候才会执行! Exec

Redis放到事务:

  • 开启事务(multi)
  • 命令入队列 (正常写命令)
  • 执行命令(exec)
127.0.0.1:6379> mu1ti#开启事务
oK
127.0.0.1:6379>set k1 v1 #命令入队
QUEUED
127.0.0.1:6379>set k2 v2
QUEUED
127.0.0.1:6379>get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379>exec# 执行事务
1) oK
2) OK
3) "v2"
4) oK
#放弃事务!
127.0.0.1:6379>multi #开启事务oK
127.0.0.1:6379>set k1 v1
QUEUED
127.0.0.1:6379>set k2 v2
QUEUED
127.0.0.1:6379>set k4 v4
QUEUED
127.0.0.1:6379> DISCARD #取消事务oK
127.0.0.1:6379>get k4 # 事务队列中命令都不会被执行!(ni1)
# 编译型异常(代码有问题!命令有错! ),事务中所有的命令都不会被执行!
127.0.0.1:6379> mu1ti
oK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 #错误的命令。编译时异常
(error)) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec #执行事务报错!
(error) EXECABORT Transaction discarded because of previous errors.

127.0.0.1:6379> get k4#所有的命令都不会被执行!
(ni1)

# 运行时异常,如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!
127.0.0.1:6379> set k1 "v1"
oK
127.0.0.1:6379> mu1ti #开启事务
oK
127.0.0.1:6379> incr k1 #会执行的时候失败! k1此时是字符串,字符串不能自增+1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range#虽然第一条命令报错了,但是依旧正常执行成功了!,这就是为什么说redis事务不保证原子性!
2) OK
3) oK
4) "v3"
127.0.0.1:6379>get k2
"v2"
127.0.0.1:6379>get k3
"v3"

Redis监控:watch

使用watch可以当做redis的乐观锁操作(面试常问)

127.0.0.1:6379> set money 100
oK
127.0.0.1:6379>set out 0
oK
127.0.0.1:6379> watch money#监视money 对象
OK
127.0.0.1:6379> multi #事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
oK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1)(integer) 80
2)(integer) 20

# 测试多进程修改值,使用watch可以当做redis的乐观锁操作!
127.0.0.1:6379> watch money #监视money
oK
127.0.0.1:6379> mu1ti
oK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> exec # 执行之前,如果另外一个进程,修改了我们的值,这个时候,就会导致事务执行失败!
(ni1)

如果修改失败,取消监控(unwatch),再重新监控(watch)获取最新的值即可

在这里插入图片描述

Redis.conf详解

启动的时候,就通过配置文件来启动!

里面有很多配置信息。

  • 网络配置
bind 127.0.0.1 #绑定的ip
protected-mode yes #保护模式
port 6379 #端口设置
  • SECURITY安全
可以在这里设置redis的密码,默认是没有密码!
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass #获取redis的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" #设置redis的密码
oK
127.0.0.1:6379> config get requirepass # 发现所有的命令都没有权限了
(error) NOAUTH Authentication required.
127.0.0.1:6379> ping
(error)NOAUTH Authentication required.

127.0.0.1:6379> auth 123456 # 使用密码进行登录!
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
  • 通用设置GENERAL
daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为yes !
pidfile ar/run/redis_6379.pid # 如果以后台的方式运行,我们就需要指定一个pid文件!

#日志
#specify the server verbosity leve1.# 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 1eve1)# notice (moderately verbose,what you want in production probably)生产环境# warning (only very important / critical messages are logged)
1oglevel notice
logfile ""   #日志的文件位置名
databases 16 #数据库的数量,默认是 16个数据库
always-show-1ogo yes #是否总是显示LOGO
  • RDB配置
#持久化
#在规定的时间内,执行了多少次操作,则会持久化到文件.rdb或.aof
#redis是内存数据库,如果没有持久化,那么数据断电及失!

#如果900s内,如果至少有一个1 key进行了修改,我们及进行持久化操作
save 900 1
#如果3005内,如果至少10 key进行了修改,我们及进行持久化操作
save 300 10
#如果60s内,如果至少10000 key进行了修改,我们及进行持久化操作
save 60 10000
#我们之后学习持久化,会自己定义这个测试!
stop-writes-on-bgsave-error yes #持久化如果出错,是否还需要继续工作!
rdbcompression yes # 是否压缩rdb 文件,需要消耗一些cpu资源!
rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验!
dir ./    #rdb文件保存的目录! 其实就是我们的/usr/local/bin
dbfilename dump.rdb #rdb保存的文件
  • 限制 CLIENTS
maxclients 10000   #设置能连接上redis的最大客户端的数量
maxmemory <bytes>  # redis配置最大的内存容量

maxmemory-policy noeviction #内存到达上限之后的处理策略
    1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
    2、allkeys-lru :删除lru算法的key
    3、volatile-random:随机删除即将过期key
    4、allkeys-random:随机删除
    5、volatile-ttl :删除即将过期的
    6、noeviction :永不过期,返回错误I
  • AOF配置(默认不开启aof模式)
appendon1y no  #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof"  # 持久化的文件的名字

#appendfsync always   #每次修改都会sync。消耗性能
appendfsync everysec  #每秒执行一次sync,可能会丢失这1s的数据!
#appendfsync no       #不执行sync,这个时候操作系统自己同步数据,速度最快!

Redis内存淘汰策略

Redis的数据已经设置了TTL,不是过期就已经删除了吗?为什么还存在所谓的淘汰策略呢?这个原因我们需要从Redis的过期策略聊起。

Redis过期策略

①定期删除
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。
Redis 默认会每一秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略:

  • 从过期字典中随机 20 个 key;
  • 删除这 20 个 key 中已经过期的 key;
  • 如果过期的 key 比率超过 1/4,那就重复步骤 1;

redis默认是每隔 100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。

②惰性删除
所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。

定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,即当你主动去查过期的key时,如果发现key过期了,就立即进行删除,不返回任何东西。

总结:定期删除是集中处理,惰性删除是零散处理。

但事实上这还是会有问题的。如果定期删除漏掉很多过期的key,我们又没有及时查,也就没有走惰性删除。这就会导致大量过期的key堆积在内存当中,使redis内存消耗殆尽。
怎么办?内存淘汰策略!

内存淘汰策略

(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
(2)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。

(3)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
(4)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。

(5)volatile-random:从已设置过期时间的数据集中随机选择数据淘汰。
(6)allkeys-random:从数据集(server.db[i].dict)中随机选择数据淘汰。从所有key随机删除。
(7)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
(8) noenviction(不驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。

这八种大体上可以分为4中,lru(删除最近最少使用的key)、lfu(删除使用频率最低的key)、random(随机删除key)、ttl(删除将过期的key)。

在 Redis.conf 配置文件中有一行maxmemory-policy,可以配置内存淘汰策略:

maxmemory-policy volatile-lru
什么是LRU?

LRU是(Least Recently Used)的缩写,即最近最少使用。是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。

Redis 使用的并不是完全LRU算法。自动驱逐的 key , 并不一定是最满足LRU特征的那个. 而是通过近似LRU算法, 抽取少量的 key 样本, 然后删除其中访问时间最古老的那个key

在 Redis 的 LRU 算法中, 可以通过设置样本(sample)的数量来调优算法精度。 通过以下指令配置:

#每次收取10个key样本
maxmemory-samples 10

为什么不使用完全LRU实现? 原因是为了节省内存。但 Redis 的行为和LRU基本上是等价的. 下面是 Redis LRU 与完全LRU算法的一个行为对比图。
在这里插入图片描述

手写LRU

要求:O(1)时间完成put和get操作。
O(1)时间查到:hash表
O(1)时间删除:双向链表
因此数据结构就选择 :LinkedHashMap

public class LRUCache extends LinkedHashMap<Integer,Integer> {
    private int capacity;

    public LRUCache(int cap) {
        super(cap,0.75f,true);
        this.capacity = cap;
    }

    public int get(int key) {
        return super.getOrDefault(key,-1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer,Integer> eldest) {
        return super.size() > capacity;//如果hash表长度大于链表容量,就该清除了。
    }

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(3);
        lruCache.put(1,1001);
        lruCache.put(2,1002);
        lruCache.put(3,1003);
        System.out.println(lruCache.keySet());

        lruCache.get(1);//1就是最近刚使用过的,会把1放到队尾。
        System.out.println(lruCache.keySet());

        lruCache.put(4,1004);//容量达到上限,删除队头元素(因为它最久未使用过的),并把4加到队尾。
        System.out.println(lruCache.keySet());
    }
}

但是这样并不是我们手写的初衷,因为这是用到了JDK封装好的数据结构,不用JDK的真正实现手写:

public class LRUCache {
    //1.定义链表节点
    class Node {
        int key;
        int value;
        Node prev;
        Node next;
        public Node(){}
        public Node(int k,int v) {
            this.key = k;
            this.value = v;
        }
    }

    //2.需要一个hashMap存放节点,便于定位
    private Map<Integer, Node> cache = new HashMap<>();
    //记录hash表元素个数
    private int size;
    //链表容量
    private int capacity;
    //链表头,尾节点标识
    private Node head;
    private Node tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        Node node = cache.get(key);
        //hash表查找该节点,如果没有就返回-1
        if (node == null) {
            return -1;
        }
        //如果key存在,先通过hash表定位,再移到头部
        moveToTail(node);
        return node.value;
    }

    public void put(int k, int v) {
        Node node = cache.get(k);
        if (node == null) {
            //如果key不存在,创建新节点
            Node newNode = new Node(k, v);
            //添加到hash表
            cache.put(k, newNode);
            //添加到双向链表
            addToTail(newNode);
            ++size;
            if (size > capacity) {
                //如果超出容量,删除双向链表的尾节点(最近最久未使用的)
                Node tail = removeHead();
                //删除hash表对应的项
                cache.remove(tail.key);
                --size;
            }
        } else {
            //如果key存在,先通过hash表定位,再修改value,并移到头部
            node.value = v;
            moveToTail(node);
        }
    }

    private void addToTail(Node node) {
        node.prev = tail.prev;
        node.next = tail;
        tail.prev.next = node;
        tail.prev = node;
    }

    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToTail(Node node) {
        removeNode(node);
        addToTail(node);
    }

    private Node removeHead() {
        Node res = head.next;
        removeNode(res);
        return res;
    }
    

}

Redis持久化

Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能!

Redis为了保证效率,数据缓存在内存中Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,以保证数据的持久化

RDB (Redis DataBase)

什么是RDB?

在这里插入图片描述
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。Redis会单独创建 ( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。

整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

rdb保存的文件dump.rdb ,在.conf文件中Snapshot配置栏中是RDB相关配置

....
dbfilename dump.rdb #rdb保存的文件
触发机制
  • save的规则满足的情况下,会自动触发rdb规则
  • 执行flushall命令,也会触发我们的rdb规则!
  • 退出redis,也会产生rdb文件!
如何恢复rdb文件?
  • 只需要将rdb文件放到我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复其中的数据!

  • 查看需要存在的位置

    127.0.0.1:6379> config get dir
    1) "dir"
    2) "/usr/loca1/bin"  #如果在这个目录下存在dump .rdb 文件,启动就会自动恢复其中的数据
    

优点:

1、fork了一条子进程,主进行继续接待client请求是不进行任何IO操作,保证了效率,适合大规模的数据恢复!

2、非常适合用于进行备份。比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适用于灾难恢复(disaster recovery)。

3、对数据的完整性要求不高!

缺点:

1、如果redis意外宕机了,那么有可能丢失好几分钟的数据。

2、fork子进程的时候,会占用一定的内存空间!

AOF (Append Only File)

默认是不开启的

将我们执行过的所有的命令都记录下来,history, 恢复的时候就把这个文件里的历史命令全部再执行一遍!

AOF是什么?

在这里插入图片描述
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据(万一是大数据量就会很慢了),换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

AOF保存的是appendonly.aof 文件

如果这个aof文件有错位,这时候redis是启动不起来的吗,我们需要修复这个aof文件

redis给我们提供了一个工具redis-check-aof --fix appendonly.aof

Redis的appendfsync参数详解

redis.conf中的appendfysnc是对redis性能有重要影响的参数之一。可取三种值:alwayseverysecno

​ appendfsync always:总是写入aof文件,并完成磁盘同步
  appendfsync everysec:每一秒写入aof文件,并完成磁盘同步

​ appendfsync no:写入aof文件,不等待磁盘同步。但是如果这个时候redis挂掉,就会丢失数据。

设置为always时,会极大消弱Redis的性能,因为这种模式下每次write后都会调用fsync(Linux为调用fdatasync)。强制磁盘同步。

如果设置为no,则write后不会有fsync调用,由操作系统自动调度刷磁盘,性能是最好的。

everysec为最多每秒调用一次fsync,这种模式性能并不是很糟糕,一般也不会产生毛刺,这归功于Redis引入了BIO线程,所有fsync操作都异步交给了BIO线程。

可见,从持久化角度讲,always是最安全的。从效率上讲,no是最快的。而redis默认设置进行了折中,选择了everysec。合情合理。

系统调用write和fsync说明:

•write操作会触发延迟写(delayed write)机制。
Linux在内核提供页缓冲区用来提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
•fsync针对单个文件操作(比如AOF文件),做强制硬盘同步,fsync将阻塞主进程直到写入硬盘完成后返回,保证了数据持久化。

  1. 为什么要调用fsync函数,不是已经调用write把数据写入到文件了吗?
  2. fsyncByPolicy有几种策略?

首先回答问题1,为什么写入文件后,还要调用fsync函数:
大多数unix系统为了减少磁盘IO,采用了**“延迟写”技术**,也就是说当我们执行完write调用后,数据并不一定立马被写入磁盘(可能还是保留在系统的buffer cache或者page cache中),这样当主机突然断电,这些我们本以为已经写入到磁盘文件的数据可能就会丢失;所以当我们需要确保数据被完整正确的写入磁盘,则需要调用同步函数fsync,它会一直阻塞主进程直到数据全部被写入到硬盘

优点:

AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。

可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。

缺点:

相对于数据文件来说,AOF远远大于RDB,修复的速度也比RDB慢!

对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。

根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB**。 **在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

二者的区别
  • RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

  • AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

RDB 和 AOF ,我应该用哪一个?
  • 如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久。
  • AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低 Redis 的性能,不知道你是否可以接受。

数据库备份和灾难恢复:定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。

Redis 支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。

问题:为什么恢复的时候RDB比AOF快?
  • AOF,存放的指令日志,做数据恢复的时候,其实是要回放和执行所有的指令日志,来恢复出来内存中的所有数据的;
  • RDB,就是一份数据文件,恢复的时候,直接加载到内存中即可;
    RDB的时候,Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可;

Redis发布/订阅

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

Redis客户端可以订阅任意数量的频道
订阅/发布消息图:


在这里插入图片描述

示例解释
PSUBSCRIBE [pattern [pattern] …]订阅一个或多个符合给定模式的频道。
除了直接订阅给定 channel 外,还可以使用
PSUBSCRIBE 订阅一个模式(pattern),
订阅一个模式等同于订阅所有匹配这个模式的 channel 。
PUBSUB subcommand查看订阅与发布系统的状态。
PUBLISH channel message将消息发送到指定频道
PUNSUBSCRIBE [pattern [pattern] …]推定所有给定模式的频道
SUBSCRIBE channel …订阅一个或多个频道的信息。
UNSUBSCRIBE channel退订给定的频道。
发布/订阅的原理

Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现,籍此加深对Redis的理解。
Redis通过PUBLISH 、SUBSCRIBE 和PSUBSCRIBE等命令实现发布和订阅功能。
通过SUBSCRIBE 命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键,就是将客户端添加到给定 channel的订阅链表中。通过 PUBLISH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub从字面上理解就是发布( Publish )与订阅(Subscribe ),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能

订阅原理:

操作:当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。

示意图:如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子,通过遍历所有输入频道。
在这里插入图片描述
可以看出,实现 SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。

发布原理:

客户端和服务端可以理解为都各自维护着一个channel列表。

PUBLISH
在这里插入图片描述
PUBLISH 命令的实现

使用 PUBLISH 命令向订阅者发送消息,需要执行以下两个步骤:

1) 使用给定的频道作为键,在 redisServer.pubsub_channels 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

2) 遍历 redisServer.pubsub_patterns 链表,将链表中的模式和给定的频道进行匹配,如果匹配成功,那么将消息发布到相应模式的客户端当中。

举个例子,假设有两个客户端分别订阅 it.news 频道和 it.* 模式,当执行命令PUBLISH it.news “hello moto” 的时候, it.news 频道的订阅者会在步骤 1 收到信息,而当PUBLISH 进行到步骤 2 的时候, it.* 模式的订阅者也会收到信息。

退订原理:

使用 UNSUBSCRIBE 命令可以退订指定的频道, 这个命令执行的是订阅的反操作: 它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。

二、REDIS发布订阅和监听REDIS队列的区别

使用jedis的subscribe和publish实现的发布订阅系统 PK 使用jedis的BRPOP和BLPOP实现的阻塞时消息队列。

1、redis队列为阻塞队列,获取完一个信息后会主动退出,如果想一直获取信息则需要开启一个监听;而发布订阅中的订阅端是自动完成的监听。

2、redis队列中的数据取出后就消失了,无法满足多端口;而发布订阅可以将数据发布到多个channel。

3、redis队列的数据不取出就会一直在缓存中;而发布订阅中订阅获取的数据不处理就消失了。

Redis主从复制

概念
**主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。**前者称为主节点(masterleader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
1、数据冗余∶主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复∶当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用(集群)基石∶除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。

在这里插入图片描述
环境配置
只配置从库,不用配置主库!

127.0.0.1:6379> info replication #查看当前服务器的信息 
# Replication
role : master # 角色 
masterconnected_slaves : 0  #没有从机
.......
  • copy出三个(具体情况而定).conf 配置文件

  • 修改每个配置文件中对应的信息:端口号,pid名字,log文件名字,dump.rdb名字等

依次启动对应服务:

redis-server redis79.conf
redis-server redis80.conf
redis-server redis81.conf

在这里插入图片描述

配置一主二从

默认情况下,每台redis服务器都是主节点;

认老大,若,主(79)从(80,81)

在从机中配置:

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host 6379 找谁当自己的老大!
oK
127.0.0.1:6380> info replication
# Replication
role:slave #当前角色是从机
master_host:127.0.0.1 #可以的看到主机的信息
master_port:6379
master__7ink_status:up
master_1ast_io_seconds_ago : 3
master_sync_in_progress:0
s1ave_rep1_offset: 14
s1ave_priority: 100
s1ave_read_only: 1
connected_s1aves :0
master_replid :a81be8dd257636b2d3e7a9f595e69d73ff03774e
master_rep7id2 :00000000000000000000o000o0000000000o000omaster_rep1_offset: 14
second_rep1_offset:-1
rep1_back1og_active:1
rep1_back1og_size:1048576
rep1_backlog_first_byte_offset:1
rep7_backlog_histlen : 14

###################################################################################
#在主机中查看!
127.0.0.1:6379> info replication
# Replication
role :master
connected_slaves : 1 #多了从机的配置
s7ave0:ip=127.0.0.1,port=6380,state=on7ine,offset=42,1ag=1 # 多了从机的配置master_replid:a81be8dd257636b2d3e7a9f595e69d73ff03774e
master_replid2:0000000000000000000000000000000000000000
master_rep1_offset:42
second_rep1_offset:-1
rep1_backlog_active:1
rep1_backlog_size : 1048576
rep1_back1og_first_byte_offset: 1
reb1_backlog_histlen :42

第二台配置同理!

细节

从机只能读,主机才能写!(如果从机写就会报错)

主机中的所有信息和数据,都会自动被从机保存!

测试∶主机断开连接,从机依旧连接到主机的,但是没有写操作了,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!

如果是使用命令而不是配置文件来配置的主从,这个时候如果重启了,就会变回主机!只要变为从机,立马就会从主机中获取值!

复制原理
Slave启动成功连接到master后会发送一个sync同步命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。

链路模式

上个主连接下一个从,下一个从继续连接下一个从

在这里插入图片描述

这时候也可以完成主从复制!

这种情况下,如果主机挂了,没有老大了,这个时候能不能选择一个老大出来呢?哨兵模式出来之前都是通过手动配置的!

如果主机断了,我可以使用这个命令,来使自己成为主节点! 其他节点也手动连接到新主节点!

SLAVEOF NO ONE 

哨兵模式(自动投票选举主机)

(自动选举老大的模式)

概述

主从切换技术的方法是∶当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其
原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例

在这里插入图片描述

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式

在这里插入图片描述

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover[故障转移]过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果随机由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

三个定时任务
sentinel在内部有3个定时任务
1)每10秒每个sentinel会对master和slave执行info命令,这个任务达到两个目的:
- 发现slave节点
- 确认主从关系
2)每2秒每个sentinel通过master节点的channel交换信息(pub/sub)。master节点上有一个发布订阅的频道(sentinel:hello)。sentinel节点通过sentinel:hello频道进行信息交换(对节点的"看法"和自身的信息),达成共识。
3)每1秒每个sentinel对其他sentinel和redis节点执行ping操作(相互监控),这个其实是一个心跳检测,是失败判定的依据。

配置

1、配置哨兵配置文件sentinel.conf。创建n个哨兵配置文件

#内容:
#语法sentinel monitor [主机名称] [host] [port] 1
sentinel monitor myredis 127.0.0.1 6379 1 

后面数字1代表,主机挂了,从机会参与选举看让谁接替成为主机,票数多的,就会成为主机!

2、启动哨兵

redis-sentinel myconfig/sentinel.conf

如果主机回来了,只能归并到新的主机下,当从机,这就是哨兵模式的规则!

优点:
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有;

2、主从可以切换,故障可以转移,系统的可用性就会更好;
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!

缺点︰
1、Redis 不好在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦!

2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!

哨兵模式的全部配置

#Example sentinel.conf
#哨兵sentine1实例运行的端口默认26379。如果有多个哨兵,需要配置多端口。

port 26379

#哨兵sentine1的工作目录

dir /tmp

#哨兵sentine1监控的redis主节点的ip port
# master-name可以自己命名的主节点名字只能由字母A-z、数字0-9、这三个字符".-_"组成。

#quorum配置多少个sentine1哨兵统一认为master主节点失联那么这时客观上认为主节点失联了

#sentine1 monitor
sentine1 monitor mymaster 127.0.0.1 6379 2

#当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密

#设置哨兵sentinel连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass
sentine7 auth-pass mymaster MySUPER–secret-o123passwOrd

#指定多少毫秒之后 主节点没有应答哨兵sentine1此时哨兵主观上认为主节点下线 默认30秒

#sentinel down-after-mi77iseconds

sentinel down-after-mi77iseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover故障转移所需的时间就越长,但是如果这个数字越大,就意味着越多的s1ave因为replication而不可用。
可以通过将这个值设为1来保证每次只有一个slave 处于不能处理命令请求的状态。

#sentinel paralle1-syncs
sentinel parallel-syncs mymaster 1
#故障转移的超时时间failover-timeout可以用在以下这些方面:

#1.同一个sentine1对同一个master两次failover之间的间隔时间。
#2.当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。

#3.当想要取消一个正在进行的fai1over所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
#默认三分钟
#sentinel failover-timeout

sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。

#对于脚本的运行结果有以下规则;
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10

#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。

#通知型脚本:当sentine1有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SwS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentine1.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentine1无法正常启动成功。
#通知脚本
# she11编程
#sentine1 notification-script

sentinel notification-script mymaster /var/redis/notify.sh
#客户端重新配置主节点参数脚本
#当一个master由于faiover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。

#以下参数将会在调用脚本时传给脚本:
# ,

#目前总是题failover",
#是“leader”或者"observer"中的一个。
#参数 from-ip,from-port,to-ip,to-port是用来和旧的master和新的master(即旧的slave)通信的

#这个脚本应该是通用的,能被多次调用,不是针对性的。
#sentinel client-reconfig-script

sentine1 client-reconfig-script mymaster /var/redis/reconfig.sh

深入理解哨兵模式:https://www.cnblogs.com/kevingrace/p/9004460.html

Redis缓存雪崩&缓存穿透&缓存击穿

(面试高频,工作常用)

Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。

缓存穿透(缓存没有,数据库又查不到)

概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候(如秒杀系统),一时间缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,导致数据库扛不住这么大的访问量,这时候就相当于出现了缓存穿透。
查询对于缓存和数据库都没有的数据的情况下。比如一些恶意的请求。
在这里插入图片描述

解决方案

①布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力﹔

在这里插入图片描述

②缓存空对象
当存储层不命中后,即使返回的是空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;

在这里插入图片描述

但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(热点key过期,大量并发打到数据库上)

微博服务器宕机(某个热搜爆了)

概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
在这里插入图片描述

解决方案

  • 设置热点数据永不过期。从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
  • 加互斥锁。 使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中过期失效。

产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

在这里插入图片描述

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

比如,停电了,redis宕机,等都会导致缓存雪崩。

解决方案

redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样十台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量分散地均匀。

redis分布式锁

详细请看:https://blog.csdn.net/qq_37781649/article/details/108814474

如果想真正了解分布式锁, 需要结合一定场景; 举个例子, 某夕夕上抢购 AirPods Pro 的 100 元优惠券
如果使用下面这段代码当作抢购优惠券的后台程序, 我们一起看一下, 可能存在什么样的问题
在这里插入图片描述
很明显的就是这段流程在并发场景下并不安全, 会导致优惠券发放超过预期, 类似电商抢购超卖问题。
想一哈有什么方式可以避免这种分布式下超量问题?
互斥加锁, Java 中互斥锁的语义就是 同一时间, 只允许一个客户端对资源进行操作
比如 Java 中的关键字 Synchronized, 以及 JUC Lock 包下的 ReentrantLock 都可以实现互斥锁。
JVM 锁
如图所示, 加入 JVM synchronized 锁确实可以解决单机下并发问题
在这里插入图片描述

但是生产环境为了保证服务高可用, 起码要 部署两台服务, 这样的话 synchronized 就不起作用了, 因为它的 作用域只是单个 JVM

分布式情况下只能通过 分布式锁 来解决多个服务资源共享的问题了!

分布式锁

分布式锁的定义:

保证同一时间只能有一个客户端对共享资源进行操作

比对刚才举的例子, 不论部署多少台优惠券服务, 只会有 一台服务能够对优惠券数量进行增删操作

另外有几点要求也是必须要满足的:

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

2、具有容错性。 只要大部分的Redis节点正常运行,客户端就可以加锁和解锁

3、解铃还须系铃人。 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了;
4、加锁和解锁必须具有原子性

分布式锁实现大致分为三种, Redis、Zookeeper、数据库, 文章以 Redis 展开分布式锁的讨论

Redis基本实现

借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
在这里插入图片描述
主要就是这三个命令:

(1)setnx

SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

(2)expire

expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

(3)delete

delete key:删除key

在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

实现思想:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

public void testLock() {
		// 执行redis的setnx命令
		String uuid = UUID.randomUUID().toString();
		Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 5, TimeUnit.SECONDS);
		// 判断是否拿到锁
		if (lock) {
			// 执行业务逻辑代码
			// ...
			// 释放锁资源 (保证获取值和删除操作的原子性) LUA脚本保证删除的原子性
			String script = "if redis.call('get', KEYS[1]) == ARGV[1] then
 return redis.call('del', KEYS[1]) else return 0 end";
 
			this.redisTemplate.execute(new DefaultRedisScript<>(script), 
Arrays.asList("lock"), Arrays.asList(uuid));
//			if (StrUtil.equals(uuid,redisTemplate.opsForValue().get("lock"))){
//				redisTemplate.delete("lock");
//			}
		} else {
			// 其他请求尝试获取锁
			testLock();
		}
	}

第二种:

/**
 * 分布式锁的简单实现代码
 * Created by liuyang on 2017/4/20.
 */
public class DistributedLock {
 
    private final JedisPool jedisPool;
 
    public DistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }
 
    /**
     * 加锁
     * @param lockName       锁的key
     * @param acquireTimeout 获取超时时间
     * @param timeout        锁的超时时间
     * @return 锁标识
     */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 获取连接
            conn = jedisPool.getResource();
            // 随机生成一个value
            String identifier = UUID.randomUUID().toString();
            // 锁名,即key值
            String lockKey = "lock:" + lockName;
            // 超时时间,上锁后超过此时间则自动释放锁
            int lockExpire = (int) (timeout / 1000);
 
            // 获取锁的超时时间,超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    // 返回value值,用于释放锁时间确认
                    retIdentifier = identifier;
                    return retIdentifier;
                }
                // 返回-1代表key没有设置超时时间,为key设置一个超时时间
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }
 
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }
 
    /**
     * 释放锁
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                // 监视lock,准备开始事务
                conn.watch(lockKey);
                // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
                if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值