Redis详解

Redis

记录Redis学习笔记,待完善~~~~

官方中文网站:http://redis.cn/ 英文网站:https://redis.io/

1、linux安装redis

1、教程

https://blog.csdn.net/qq_39135287/article/details/83474865

2、测试

启动服务

redis-server /home/redis/redis-6.2.6/etc/redis.conf

进入bin目录,输入redis-cli -p 6379 -a 123456

[root@localhost bin]# redis-cli -p 6379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> ping
PONG
127.0.0.1:6379>

返回PONG,则表示安装成功,连接成功!

2、基本命令

1、查看所有的key
keys *
2、设置key
set name lisi
3、查看指定的值
get name
4、切换数据库
select 4 【默认使用的数据库是第0 个】
5、从当前数据库中删除所有的key
flushdb
6、删除所有数据库中的key
flushall
7、查看key是否存在,存在返回1,否则返回0
exists name
8、移除key
move naem lisi
9、设置key 的过期时间,单位毫秒
expire age 10
#查看还剩几秒
ttl age
10、判断key的类型
type name

3、五大数据类型

1、String字符串类型
del lcp #删除
127.0.0.1:6379> set name lcp  #设置值
OK
127.0.0.1:6379> get name #获得值
"lcp"
127.0.0.1:6379> keys *  
1) "name"
127.0.0.1:6379> exists name  #判断一个key是否存在
(integer) 1
127.0.0.1:6379> exists lcp  # 存在返回1 ,负责返回0
(integer) 0
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> append name "hello"  	#追加字符串,如果该key不存在,则相当于setkey
(integer) 8
127.0.0.1:6379> get name 
"lcphello"
127.0.0.1:6379> strlen name #获取字符串长度
(integer) 8
127.0.0.1:6379> append name "nihao"
(integer) 13
127.0.0.1:6379> strlen name
(integer) 13
127.0.0.1:6379> get name
"lcphellonihao"
#i++
#步长 i+=
#incr :自增
127.0.0.1:6379> SET views 0 #初始量为0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views #自增1 浏览量为+1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views # 自减-1 浏览量-1
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incrby views 10 #步长每次+10
(integer) 10
127.0.0.1:6379> get views
"10"
127.0.0.1:6379> decrby views 10 #步长每次-10
(integer) 0
127.0.0.1:6379> get views
"0"
#字符串范围 range
127.0.0.1:6379> set key1 "hello,redis"
OK
127.0.0.1:6379> get key1
"hello,redis"
127.0.0.1:6379> getrange key1 1 4 #字符串截取 【1,4】
"ello"
127.0.0.1:6379> get key1
"hello,redis"
127.0.0.1:6379> getrange key1 2 6
"llo,r"
127.0.0.1:6379> getrange key1 0 -1 #截取所有,相当于get key
"hello,redis"
127.0.0.1:6379> set key1 abdc 
OK
127.0.0.1:6379> get key1 #字符串替换
"abdc"
127.0.0.1:6379> setrange key1 1 xx #替换指定位置开始的字符串
(integer) 4
127.0.0.1:6379> get key1
"axxc"
#setex 设置过期时间
#TimeUnit.DAYS         日的工具类  
#TimeUnit.HOURS        时的工具类  
#TimeUnit.MINUTES      分的工具类  
#TimeUnit.SECONDS      秒的工具类  
#TimeUnit.MILLISECONDS 毫秒的工具类  
#setnx 不存在则设置
127.0.0.1:6379> setex key1 10 "hello" #设置key1的值为hello,10秒后过期
OK
127.0.0.1:6379> get key1
"hello"
127.0.0.1:6379> ttl key1
(integer) -2
127.0.0.1:6379> get key1
(nil)
127.0.0.1:6379> setnx myredis "redis" #如果myredis不存在则创建
(integer) 1
127.0.0.1:6379> get myredis
"redis"
127.0.0.1:6379> setnx myredis "mo" #如果myredis存在则创建失败!成功返回1,负责返回0
(integer) 0
127.0.0.1:6379> get myredis
"redis"
127.0.0.1:6379> 
#mset
#mget
127.0.0.1:6379> mset key1 n1 key2 n2 key3 n3 #同时设置多个值
OK
127.0.0.1:6379> keys *
1) "key1"
2) "key3"
3) "key2"
127.0.0.1:6379> mget key1 key2 key3 #同时获取多个值
1) "n1"
2) "n2"
3) "n3"
127.0.0.1:6379> msetnx key1 aa key5 12 #msetnx 是一个原子性操作,要么全部成功,要么全部失败
(integer) 0
127.0.0.1:6379> get key5
(nil)

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

#这里key是一个巧妙的设计:user:{id}:{filed},如此设计在Redis中是完全ok的
127.0.0.1:6379> clear
127.0.0.1:6379> mset user:1:name lisi user:1:age 12
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "lisi"
2) "12"
127.0.0.1:6379> 

##############################################################
#getset 先get在set
127.0.0.1:6379> getset db redis #如果值不存在,则返回(nil)
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db aa #如果值存在,则获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"aa"
127.0.0.1:6379> 

使用场景

  • 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  • 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  • 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。
2、list列表类型

在redis里面,可以把list完成队列,栈,阻塞队列

所有的list命令都以l开头

127.0.0.1:6379> lpush list a #将一个值或多个值,插入到列表的头部(左)
(integer) 1
127.0.0.1:6379> lpush list b
(integer) 2
127.0.0.1:6379> lpush list c
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #获取list列表的值  
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> lrange list 0 1 #通过区间获取具体的值 先进后出的形式展示
1) "c"
2) "b"
127.0.0.1:6379> rpush list d  #将一个值或多个值,插入到列表的尾部(右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1 #先进后出尾部是a
1) "c"
2) "b"
3) "a"
4) "d"
#lpop
#rpop
127.0.0.1:6379> lpop list #移除list的第一个元素
"c"
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"
3) "d"
127.0.0.1:6379> rpop list #移除list的最后元素
"d"
127.0.0.1:6379> lrange list 0 -1
1) "b"
2) "a"

#######################################################################
#lindex
127.0.0.1:6379> lindex list 0 #通过下标获取list中的某一个值
"b"
127.0.0.1:6379> lindex list 1
"a"

#######################################################################
#llen
127.0.0.1:6379> lpush list aa
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "aa"
2) "b"
3) "a"
127.0.0.1:6379> llen list #获取列表的长度
(integer) 3
#lrem :移除指定的值
127.0.0.1:6379> lpush list a
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "a"
2) "aa"
3) "b"
4) "a"
127.0.0.1:6379> lrem list 1 aa #移除List中指定个数的Value,精准匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "a"
2) "b"
3) "a"
127.0.0.1:6379> lrem list 2 a #移除两个value是a的
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "b"
#ltrim 修剪 list 截断
127.0.0.1:6379> lpush list a
(integer) 2
127.0.0.1:6379> lpush list c
(integer) 3
127.0.0.1:6379> lpush list d
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "d"
2) "c"
3) "a"
4) "b"
127.0.0.1:6379> ltrim list 1 2#通过下标截取指定的长度,这个list已经被改变了,截断了只剩下截取的元素
OK
127.0.0.1:6379> lrange list 0 -1
1) "c"
2) "a"
#rpoplpush :移除列表最后一个元素,将他移动到新的列表中
127.0.0.1:6379> lpush list a
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "a"
2) "c"
3) "a"
127.0.0.1:6379> rpoplpush list mylist #移除列表最后一个元素,将他移动到新的列表中
"a"
127.0.0.1:6379> lrange list 0 -1 #查看原来的列表
1) "a"
2) "c"
127.0.0.1:6379> lrange mylist 0 -1 #查看目标列表,确实有了元素
1) "a"
127.0.0.1:6379> 
#lset:将列表指定下表的值替换成新的值,更新操作
127.0.0.1:6379> exists list #判断该集合是否存在
(integer) 1
127.0.0.1:6379> exists list2
(integer) 0
127.0.0.1:6379> lrange list 0 -1
1) "a"
2) "c"
127.0.0.1:6379> lset list2 0 a  #如果列表不存在,更新则会报错
(error) ERR no such key
127.0.0.1:6379> lset list 1 bb #如果存在,更新列表中下标的值
OK
127.0.0.1:6379> lrange list 0 -1 
1) "a"
2) "bb"
127.0.0.1:6379> lset list 3 aa #如果该列表中的值不存在,则会报错
(error) ERR index out of range

#linsert:将某个value插入到列表中某个元素的前面或后面
127.0.0.1:6379> lrange list 0 -1
1) "a"
2) "bb"
127.0.0.1:6379> lpush list c
(integer) 3
127.0.0.1:6379> linsert list before a e #在元素a的前面插入e
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "c"
2) "e"
3) "a"
4) "bb"
127.0.0.1:6379> linsert list after a ff #在元素a的后面插入ff
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "c"
2) "e"
3) "a"
4) "ff"
5) "bb"

使用场景

  • 最新列表,List 类型的 lpush 命令和 lrange 命令能实现最新列表的功能,每次通过 lpush 命令往列表里插入新的元素,然后通过 lrange 命令读取最新的元素列表,如朋友圈的点赞列表、评论列表:

​ 1、A关注了B,C等大V

​ 2、B发微博了,消息ID为1001:LPUSH msg:{A的ID} 1001

​ 3、C发微博了,消息ID为1002:LPUSH msg:{A的ID} 1002

​ 4、A查看最新的5条微博消息:LRANGE msg:{A的ID} 0 5

按照时间排序的这些朋友圈信息等

3、set集合类型

set中的值,不能重复

#sadd set集合中添加元素
127.0.0.1:6379> sadd myset a1  #set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset a2
(integer) 1
127.0.0.1:6379> sadd myset a3
(integer) 1
127.0.0.1:6379> smembers myset  #查看set集合中的所有元素
1) "a2"
2) "a1"
3) "a3"
127.0.0.1:6379> sismember myset a1  #判断某一个值是否在set集合中
(integer) 1
127.0.0.1:6379> sismember myset a5
(integer) 0
##################################################################
#scard 获取set集合元素中的个数
127.0.0.1:6379> scard myset  #获取set集合元素中的个数
(integer) 3
#srem :移除set元素中指定的元素
127.0.0.1:6379> srem myset a1  #移除set元素中指定的元素
(integer) 1
127.0.0.1:6379> smembers myset
1) "a2"
2) "a3"
#srandmember 随机抽取set中的元素
127.0.0.1:6379> SRANDMEMBER myset 1  #随机抽取set元素中的某一个元素
1) "a3"
127.0.0.1:6379> srandmember myset 2  #随机抽取set元素中指定的个数
1) "a1"
2) "a3"
127.0.0.1:6379> srandmember myset 1
1) "a4"
#spop:随机删除set集合中的元素
127.0.0.1:6379> SMEMBERS myset
1) "a4"
2) "a1"
3) "a2"
4) "a3"
127.0.0.1:6379> spop myset #随机删除set集合中的某一个元素
"a1"
127.0.0.1:6379> smembers myset
1) "a4"
2) "a2"
3) "a3"
127.0.0.1:6379> spop myset 2 #随机删除set集合中指定个数的元素
1) "a2"
2) "a3"
127.0.0.1:6379> smembers myset
1) "a4"
127.0.0.1:6379> 
#smove :将一个指定的值,移动到另一个set集合
127.0.0.1:6379> smembers myset
1) "a2"
2) "a1"
3) "a4"
4) "a3"
127.0.0.1:6379> smove myset myset1 a2 #将一个指定的值,移动到另一个set集合
(integer) 1
127.0.0.1:6379> smembers myset
1) "a1"
2) "a4"
3) "a3"
127.0.0.1:6379> smembers myset1
1) "a2"
127.0.0.1:6379> 
微博,B站,共同关注(并集)
#交集 都有的 sinter
#并集 全部 sunion
#差集  sdiff

127.0.0.1:6379> smembers myset
1) "a2"
2) "a1"
3) "a4"
4) "a3"
127.0.0.1:6379> smembers myset1
1) "a2"
2) "a5" 
127.0.0.1:6379> sdiff myset myset1  #差集
1) "a4"
2) "a1"
3) "a3"
127.0.0.1:6379> sinter myset myset1  #交集  共同好友就可以这样实现
1) "a2"
127.0.0.1:6379> sunion myset myset1 #并集
1) "a3"
2) "a2"
3) "a1"
4) "a4"
5) "a5"

模拟微博用户点赞 假设1001为:微博ID ,a1为:用户ID

127.0.0.1:6379> sadd 1001 a1 #用户ID为a1点赞了微博
(integer) 1 
127.0.0.1:6379> sadd 1001 a2 #用户ID为a2点赞了微博
(integer) 1
127.0.0.1:6379> SMEMBERS 1001 #查看所有的点赞用户
1) "a1"
2) "a2"
127.0.0.1:6379> scard 1001 #点赞数
(integer) 2
127.0.0.1:6379> Srem 1001 a1 #用户ID为a1的取消点赞
(integer) 1
127.0.0.1:6379> SMEMBERS 1001  #查看所有的点赞用户
1) "a2"
127.0.0.1:6379> SISMEMBER 1001 a1 #是否点赞
(integer) 0
127.0.0.1:6379> SISMEMBER 1001 a2  #是否点赞
(integer) 1

使用场景

  • 给用户添加标签,跟我们上面的例子一样。一个人对应多个不同的标签。
  • 好友/关注/粉丝/感兴趣的人集合,可以使用上面的取交集、并集相关的命令。
  • 随机展示,通过 srandmember 随机返回对应的内容,像一些首页获取动态内容可以这么玩。
  • 黑名单/白名单,有业务出于安全性方面的考虑,需要设置用户黑名单、ip 黑名单、设备黑名单等,set 类型适合存储这些黑名单数据,sismember 命令可用于判断用户、ip、设备是否处于黑名单之中。
  • 微信抽奖小程序

​ 1、点击参与抽奖加入集合:SADD item:1001 {userID}

​ 2、查看参与抽奖的所有用户:SMEMBERS item:1001

​ 3、抽取3名中奖者:SRANDMEMBER/SPOP item:1001 3 (SRANDMEMBER后,中奖的用户不会从集合中移除,SPOP 后,中奖的用户会从集合中移除)

4、Hash(哈希)

Map集合,key-map 这个值是一个map集合,本质和String类型没有太大区别,还是一个简单的key-value

#hset 集合名称 key value :hset myhash name lcp
127.0.0.1:6379> hset myhash name lcp  #set 一个具体的key -value
(integer) 1
127.0.0.1:6379> hget myhash name #获取一个字段
"lcp"
127.0.0.1:6379> hset myhash age 12
(integer) 1
127.0.0.1:6379> hget myhash age
"12"
127.0.0.1:6379> hgetall myhash #获取集合全部的数据,以键值对的形式展示
1) "name"
2) "lcp"
3) "age"
4) "12"
127.0.0.1:6379> hmget myhash name age #获取多个字段值
1) "lcp"
2) "12"
127.0.0.1:6379> hmset myhash a1 a1 a2 a2  #set 多个key -value
OK
127.0.0.1:6379> hmget myhash a1 a2
1) "a1"
2) "a2"
################################################################
#hdel 删除hash指定的key,对应的value也删除了
127.0.0.1:6379> hdel myhash a1 #删除hash指定的key,对应的value也删除了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "name"
2) "lcp"
3) "age"
4) "12"
5) "a2"
6) "a2"
##################################################################
#hlen :获取hash表的字段数量
127.0.0.1:6379> hlen myhash #获取hash表的字段数量
(integer) 3
127.0.0.1:6379> hgetall myhash
1) "name"
2) "lcp"
3) "age"
4) "12"
5) "a2"
6) "a2"
127.0.0.1:6379> HEXISTS myhash 12 #判断hash中指定的字段直是否存在
(integer) 0
127.0.0.1:6379> HEXISTS myhash name
(integer) 1
#只获取所有的key
#只获取所有的value
127.0.0.1:6379> hkeys myhash #只获取所有的key
1) "name"
2) "age"
3) "a2"
127.0.0.1:6379> HVALS myhash #只获取所有的value
1) "lcp"
2) "12"
3) "a2"
#自增
127.0.0.1:6379> HINCRBY myhash age 1
(integer) 13
127.0.0.1:6379> hget myhash age
"13"
127.0.0.1:6379> hsetnx myhash namea 123 #如果不存在则设置
(integer) 1
127.0.0.1:6379> hsetnx myhash namea 123 #如果存在则不设置
(integer) 0

hash变更的数据,尤其是用户信息之类的,经常变动的信息,hash更适合与于对象的存储,String更加适合字符串的储存

使用场景

  • 适用于存储对象,比如把用户的信息存到hash里,以用户id为key,用户的详细信息为value。
  • 电商购物车,以用户ID为key,商品ID为field,商品数量为value。
5、Zset(有序集合)

在set的基础上,增加了一个值,set k1 a1 zset myset score v1

#zadd 添加值
127.0.0.1:6379> zadd myset 1 a #添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 b 3 c  #添加多个值
(integer) 2
127.0.0.1:6379> zrange myset 0 -1 #查看所有值	
1) "a"
2) "b"
3) "c"
#zrangebyscore 显示全部数据
127.0.0.1:6379> zadd mouth 100 lcp
(integer) 1
127.0.0.1:6379> zadd mouth 1000 lidi
(integer) 1
127.0.0.1:6379> zadd mouth 4999 zhangsan
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE mouth -inf +inf #显示全部的数据 从小到大
1) "lcp"
2) "lidi"
3) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE mouth -inf +inf withscores  #显示全部的数据从小到大,并附带显示value
1) "lcp"
2) "100"
3) "lidi"
4) "1000"
5) "zhangsan"
6) "4999"
127.0.0.1:6379> ZRANGEBYSCORE mouth -inf 1000 withscores  #显示钱小于1000的用户,升序排序
1) "lcp"
2) "100"
3) "lidi"
4) "1000"
127.0.0.1:6379> ZREVRANGE phone 0 -1 withscores #显示全部的数据从大到小,并附带显示value
1) "iphone"
2) "5000"
3) "huawei"
4) "3000"
5) "oppo"
6) "2000"
7) "vivo"
8) "1000"
#zrem:移除有序集合中的指定元素
127.0.0.1:6379> zrem mouth lcp #移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange mouth 0 -1
1) "lidi"
2) "zhangsan"
#zcount :获取指定区间的成员数量
127.0.0.1:6379> zadd mouth 1000 lisi
(integer) 1
127.0.0.1:6379> zadd mouth 2000 zhangsan
(integer) 1
127.0.0.1:6379> zadd mouth 3000 xiaohong
(integer) 1
127.0.0.1:6379> zcount mouth 1000 2000  #获取指定区间的成员数量
(integer) 2
127.0.0.1:6379> zrange mouth 0 -1
1) "lisi"
2) "zhangsan"
3) "xiaohong"

案例思路:set排序 存储班级成绩表 ,工资表排序

普通消息,1、重要信息 2、带权重进行判断

排行榜应用实现

使用场景

  • 标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。
  • 共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。
  • 统计网站的独立 IP。利用 set 集合当中元素不唯一性,可以快速实时统计访问网站的独立 IP。
  • 统计用户的点赞/取消点赞
  • 排行榜功能,比如展示获取赞数最多的十个用户

4、三大特殊数据类型

1、Hyperloglog基数统计
1、简介

Redis2.8.9版本就更新了Hyperloglog数据结构!

Redis Hyperloglog 基数统计算法

优点:占用的内存是固定的,只需要12KB内存!如果要从内存的角度来比较的话Hyperloglog首选!

网页的UV(一个人访问一个网站多次,但是还是算作一个人!)

传统的方式,set保护用户的id,然后就可以统计set中的元素数量作为标准判断!

这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保护用户id;

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

2、测试使用
#两组中有相同的元素,只算一个元素
#例如,一个人多次访问一个网站,网站的浏览量只算一个人只访问了一次
127.0.0.1:6379> pfadd mykey a b #创建第一组的元素 mykey
(integer) 1
127.0.0.1:6379> pfadd myke2 a  #创建第二组的元素 myke2
(integer) 1
127.0.0.1:6379> pfcount mykey  #统计第一组元素的个数
(integer) 2
127.0.0.1:6379> pfcount myke2 #统计第二组元素的个数
(integer) 1
127.0.0.1:6379> PFMERGE mykey3 mykey myke2  #	合并两组 Mykey myke2 => mykey3并集
OK
127.0.0.1:6379> pfcount mykey3 #查看并集的数量
(integer) 2

如果允许容错,那么一定可以使用Hyperloglog!

如果不允许容错,就使用set或者自己的数据类型即可!

2、Bitmap
1、位存储

统计用户信息,活跃,不活跃!登录,未登录!打卡,未打卡!两个状态的,都可以使用Bitmap!

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

2、测试

使用bitmap来记录周一到周五的打卡!

周一:1,周二0,周三1 …

127.0.0.1:6379> setbit qd 0 1
(integer) 0
127.0.0.1:6379> setbit qd 1 0
(integer) 0
127.0.0.1:6379> setbit qd 2 0
(integer) 0
127.0.0.1:6379> setbit qd 3 1
(integer) 0
127.0.0.1:6379> setbit qd 4 0
(integer) 0
127.0.0.1:6379> setbit qd 5 1
(integer) 0
127.0.0.1:6379> setbit qd 6 0
(integer) 0

查看某一天是否有打卡

127.0.0.1:6379> getbit qd 1
(integer) 0
127.0.0.1:6379> getbit qd 5
(integer) 1

查看一周的所有打卡记录,统计天数

127.0.0.1:6379> bitcount qd
(integer) 3

5、事务

1、简介

Redis事务本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!

一次性,顺序性,排他性!执行一些列的命令!

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

Redis事务没有隔离级别的概念,也就是没有脏读,幻读,不可读

所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行

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

redis的事务

  • 开启事务(multi)
  • 命令入队()
  • 执行事务(exec)
2、正常执行事务
127.0.0.1:6379> multi  #开启事务
OK
#命令入队
127.0.0.1:6379(TX)> set k1 a1
QUEUED
127.0.0.1:6379(TX)> set k2 a2
QUEUED
127.0.0.1:6379(TX)> set k3 a3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) OK
4) "a3"
3、取消事务
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set k1 a1
QUEUED
127.0.0.1:6379(TX)> 
127.0.0.1:6379(TX)> set k2 a2
QUEUED
127.0.0.1:6379(TX)> set k3 a3
QUEUED
127.0.0.1:6379(TX)> DISCARD #取消事务
OK
127.0.0.1:6379> get k3  #事务队列中的命令都不会被执行
(nil)
4、编译型异常

代码有问题!命令有错!),事务中所有的命令都不会被执行!

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 a1
QUEUED
127.0.0.1:6379(TX)> set k2 a2
QUEUED
127.0.0.1:6379(TX)> set k3 a3
QUEUED
127.0.0.1:6379(TX)> setget k4  #错误的命令
(error) ERR unknown command `setget`, with args beginning with: `k4`, 
127.0.0.1:6379(TX)> set k5 a5
QUEUED
127.0.0.1:6379(TX)> exec #执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k5  #所有的命令都不会被执行!
(nil)
5、运行时异常

(1/0)如果事务队列中存在语法性,那么执行命令的时候,其他的命令是可以正常执行的,错误命令抛出异常

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 lisi
QUEUED
127.0.0.1:6379(TX)> incr k1 #会执行的时候失败
QUEUED
127.0.0.1:6379(TX)> set k2 a2
QUEUED
127.0.0.1:6379(TX)> set k3 a3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) (error) ERR value is not an integer or out of range #虽然有一条命令报错了,但也不会影响其他命令的执行
3) OK
4) Ok

6、Redis实现乐观锁

1、简介

监控 Watch (相当于之前在数据库中定义了version字段,他会自动检测,更新前数据是否发生变化)

悲观锁:

  • 很悲观,认为什么时候都会出问题,无论做什么都会加锁!

乐观锁:

  • 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候判断一下,在此期间是否有人修改过这个数据
  • 以前的操作获取version
  • 更新的时候比较version
2、Redis监视测试

正常执行成功!

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

注意:一个对象监视成功之后(也就是事务执行成功,该对象的监视会自动解除,下次使用需要自己手动再次监视)

测试多线程修改值,使用watch可以当作redis的乐观锁操作!

127.0.0.1:6379> set monty 100
OK
127.0.0.1:6379> set out 10
OK
127.0.0.1:6379> WATCH monty #监视 monty对象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby monty 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec #执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失败!
(nil)

另外一个线程

127.0.0.1:6379> get monty #修改了monty对象的值
"100"
127.0.0.1:6379> set monty 1000
OK

解决方案,如果修改失败,获取最新的值就好了

127.0.0.1:6379> UNWATCH #如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379> WATCH monty  #获取最新的值,再次监视,select version
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> DECRBY monty 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> exec #比对监视的值是否发生了变化,如果没有发生变化,那么可以执行成功,如果发生变化则执行失败!
1) (integer) 990
2) (integer) 20

7、Jedis

使用Java操作Redis

什么是jedis是Redis官方推荐的java连接工具!使用Java操作Redis中间件!如果你要 使用java操作Redis,那么一定要对Jedis十分熟悉

测试

1、导入依赖
 <dependencies>
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.1.1</version>
        </dependency>
<!--        日志-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
2、测试
  Jedis jedis = new Jedis("192.168.90.128",6379);
        jedis.auth("123456"); //连接redis 密码
        System.out.println(jedis.ping());
        System.out.println(jedis.keys("*"));

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kAMXwBWH-1660633441037)(C:\Users\请叫我鹏鹏君\AppData\Roaming\Typora\typora-user-images\image-20220314201858300.png)]

ok,返回pong 表示连接成功!

jedis所有的方法都是redis的命令

3、整合事务
Jedis jedis = new Jedis("192.168.90.128",6379);
        jedis.auth("123456");
        JSONObject object = new JSONObject();
        object.put("hello","world");
        object.put("name","haiyang");
        //开启事务
        Transaction multi = jedis.multi();
        String string = object.toJSONString();
        try {
            //进入队列
            multi.set("user1",string);
            multi.set("user2",string);
            //执行事务
            multi.exec();
        }catch (Exception e){
            //事务执行过程中发生异常,放弃事务
            multi.discard();
            e.printStackTrace();
        }finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();//关闭连接
        }

8、Redis整合SpringBoot

在springboot2.x之后,原来使用的jedis被替换lettuce

Jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用jedis pool连接池!更像BIO模式

lettuce:采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式

1、导入依赖
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
2、配置文件
spring.redis.host=192.168.90.128
spring.redis.port=6379
spring.redis.password=123456
3、测试
  @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        connection.flushAll();
        redisTemplate.opsForValue().set("a1","v1");
        //注意这里的操作都是ops:操作什么...
        redisTemplate.opsForList();//操作list集合 
        redisTemplate.opsForHash();
        redisTemplate.opsForSet();
        redisTemplate.opsForZSet();
        System.out.println(redisTemplate.opsForValue().get("a1"));

9、自定义RedisTemplate

源码分析:

	@Bean
@ConditionalOnMissingBean(name = "redisTemplate") 
// 我们可以自己定义一个redisTemplate来替换这个默认的!
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化!
// 两个泛型都是 Object, Object 的类型,我们后使用需要强制转换 <String, Object>
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}
@Bean
@ConditionalOnMissingBean // 由于 String 是redis中最常使用的类型,所以说单独提出来了一个bean!
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}
1、编写自定义

默认的序列化器是采用JDK序列化器,惊奇发现全是乱码,可是程序中可以正常输出。这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码

解决方案:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
    // 这是我给大家写好的一个固定模板,大家在企业中,拿去就可以直接使用!
    // 自己定义了一个 RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 我们为了自己开发方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String,
        Object>();
        template.setConnectionFactory(factory);

    // Json序列化配置
    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);
    // String 的序列化
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

    // key采用String的序列化方式
    template.setKeySerializer(stringRedisSerializer);
    // hash的key也采用String的序列化方式
    template.setHashKeySerializer(stringRedisSerializer);
    // value序列化方式采用jackson
    template.setValueSerializer(jackson2JsonRedisSerializer);
    // hash的value序列化方式采用jackson
    template.setHashValueSerializer(jackson2JsonRedisSerializer);
    template.afterPropertiesSet();

    return template;
}
}

如果用到String类型设置自增可能会报错

解决方法

package edu.hunnan.net.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
    // 这是我给大家写好的一个固定模板,大家在企业中,拿去就可以直接使用!
    // 自己定义了一个 RedisTemplate
    /**
     * 设置 redisTemplate 的序列化设置
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 1.创建 redisTemplate 模版
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        // 2.关联 redisConnectionFactory
        template.setConnectionFactory(redisConnectionFactory);
        // 3.创建 序列化类
        GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
        // 6.序列化类,对象映射设置
        // 7.设置 value 的转化格式和 key 的转化格式
        template.setValueSerializer(genericToStringSerializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}
2、Redis工具类

直接用RedisTemplate操作Redis,需要很多行代码,因此直接封装好一个RedisUtils,这样写代码更方便点。这个RedisUtils交给Spring容器实例化,使用时直接注解注入。

package edu.hunnan.net.util;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

/**
 * Redis工具类
 * @author ZENG.XIAO.YAN
 * @date   2018年6月7日
 */
@Component
public final class RedisUtil {
	
	@Autowired
	private RedisTemplate<String, Object> redisTemplate;

	// =============================common============================
	/**
	 * 指定缓存失效时间
	 * @param key 键
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean expire(String key, long time) {
		try {
			if (time > 0) {
				redisTemplate.expire(key, time, TimeUnit.SECONDS);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 根据key 获取过期时间
	 * @param key 键 不能为null
	 * @return 时间(秒) 返回0代表为永久有效
	 */
	public long getExpire(String key) {
		return redisTemplate.getExpire(key, TimeUnit.SECONDS);
	}

	/**
	 * 判断key是否存在
	 * @param key 键
	 * @return true 存在 false不存在
	 */
	public boolean hasKey(String key) {
		try {
			return redisTemplate.hasKey(key);
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 删除缓存
	 * @param key 可以传一个值 或多个
	 */
	@SuppressWarnings("unchecked")
	public void del(String... key) {
		if (key != null && key.length > 0) {
			if (key.length == 1) {
				redisTemplate.delete(key[0]);
			} else {
				redisTemplate.delete(CollectionUtils.arrayToList(key));
			}
		}
	}

	// ============================String=============================
	/**
	 * 普通缓存获取
	 * @param key 键
	 * @return 值
	 */
	public Object get(String key) {
		return key == null ? null : redisTemplate.opsForValue().get(key);
	}

	/**
	 * 普通缓存放入
	 * @param key 键
	 * @param value 值
	 * @return true成功 false失败
	 */
	public boolean set(String key, Object value) {
		try {
			redisTemplate.opsForValue().set(key, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

	}

	/**
	 * 普通缓存放入并设置时间
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
	 * @return true成功 false 失败
	 */
	public boolean set(String key, Object value, long time) {
		try {
			if (time > 0) {
				redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
			} else {
				set(key, value);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 递增
	 * @param key 键
	 * @param delta 要增加几(大于0)
	 * @return
	 */
	public long incr(String key, long delta) {
		if (delta < 0) {
			throw new RuntimeException("递增因子必须大于0");
		}
		return redisTemplate.opsForValue().increment(key, delta);
	}

	/**
	 * 递减
	 * @param key 键
	 * @param delta 要减少几(小于0)
	 * @return
	 */
	public long decr(String key, long delta) {
		if (delta < 0) {
			throw new RuntimeException("递减因子必须大于0");
		}
		return redisTemplate.opsForValue().increment(key, -delta);
	}

	// ================================Map=================================
	/**
	 * HashGet
	 * @param key 键 不能为null
	 * @param item 项 不能为null
	 * @return 值
	 */
	public Object hget(String key, String item) {
		return redisTemplate.opsForHash().get(key, item);
	}

	/**
	 * 获取hashKey对应的所有键值
	 * @param key 键
	 * @return 对应的多个键值
	 */
	public Map<Object, Object> hmget(String key) {
		return redisTemplate.opsForHash().entries(key);
	}

	/**
	 * HashSet
	 * @param key 键
	 * @param map 对应多个键值
	 * @return true 成功 false 失败
	 */
	public boolean hmset(String key, Map<String, Object> map) {
		try {
			redisTemplate.opsForHash().putAll(key, map);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * HashSet 并设置时间
	 * @param key 键
	 * @param map 对应多个键值
	 * @param time 时间(秒)
	 * @return true成功 false失败
	 */
	public boolean hmset(String key, Map<String, Object> map, long time) {
		try {
			redisTemplate.opsForHash().putAll(key, map);
			if (time > 0) {
				expire(key, time);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 向一张hash表中放入数据,如果不存在将创建
	 * @param key 键
	 * @param item 项
	 * @param value 值
	 * @return true 成功 false失败
	 */
	public boolean hset(String key, String item, Object value) {
		try {
			redisTemplate.opsForHash().put(key, item, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 向一张hash表中放入数据,如果不存在将创建
	 * @param key 键
	 * @param item 项
	 * @param value 值
	 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
	 * @return true 成功 false失败
	 */
	public boolean hset(String key, String item, Object value, long time) {
		try {
			redisTemplate.opsForHash().put(key, item, value);
			if (time > 0) {
				expire(key, time);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 删除hash表中的值
	 * @param key 键 不能为null
	 * @param item 项 可以使多个 不能为null
	 */
	public void hdel(String key, Object... item) {
		redisTemplate.opsForHash().delete(key, item);
	}

	/**
	 * 判断hash表中是否有该项的值
	 * @param key 键 不能为null
	 * @param item 项 不能为null
	 * @return true 存在 false不存在
	 */
	public boolean hHasKey(String key, String item) {
		return redisTemplate.opsForHash().hasKey(key, item);
	}

	/**
	 * hash递增 如果不存在,就会创建一个 并把新增后的值返回
	 * @param key 键
	 * @param item 项
	 * @param by 要增加几(大于0)
	 * @return
	 */
	public double hincr(String key, String item, double by) {
		return redisTemplate.opsForHash().increment(key, item, by);
	}

	/**
	 * hash递减
	 * @param key 键
	 * @param item 项
	 * @param by 要减少记(小于0)
	 * @return
	 */
	public double hdecr(String key, String item, double by) {
		return redisTemplate.opsForHash().increment(key, item, -by);
	}

	// ============================set=============================
	/**
	 * 根据key获取Set中的所有值
	 * @param key 键
	 * @return
	 */
	public Set<Object> sGet(String key) {
		try {
			return redisTemplate.opsForSet().members(key);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 根据value从一个set中查询,是否存在
	 * @param key 键
	 * @param value 值
	 * @return true 存在 false不存在
	 */
	public boolean sHasKey(String key, Object value) {
		try {
			return redisTemplate.opsForSet().isMember(key, value);
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将数据放入set缓存
	 * @param key 键
	 * @param values 值 可以是多个
	 * @return 成功个数
	 */
	public long sSet(String key, Object... values) {
		try {
			return redisTemplate.opsForSet().add(key, values);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 将set数据放入缓存
	 * @param key 键
	 * @param time 时间(秒)
	 * @param values 值 可以是多个
	 * @return 成功个数
	 */
	public long sSetAndTime(String key, long time, Object... values) {
		try {
			Long count = redisTemplate.opsForSet().add(key, values);
			if (time > 0)
				expire(key, time);
			return count;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 获取set缓存的长度
	 * @param key 键
	 * @return
	 */
	public long sGetSetSize(String key) {
		try {
			return redisTemplate.opsForSet().size(key);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 移除值为value的
	 * @param key 键
	 * @param values 值 可以是多个
	 * @return 移除的个数
	 */
	public long setRemove(String key, Object... values) {
		try {
			Long count = redisTemplate.opsForSet().remove(key, values);
			return count;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}
	// ===============================list=================================

	/**
	 * 获取list缓存的内容
	 * @param key 键
	 * @param start 开始
	 * @param end 结束 0 到 -1代表所有值
	 * @return
	 */
	public List<Object> lGet(String key, long start, long end) {
		try {
			return redisTemplate.opsForList().range(key, start, end);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 获取list缓存的长度
	 * @param key 键
	 * @return
	 */
	public long lGetListSize(String key) {
		try {
			return redisTemplate.opsForList().size(key);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 通过索引 获取list中的值
	 * @param key 键
	 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
	 * @return
	 */
	public Object lGetIndex(String key, long index) {
		try {
			return redisTemplate.opsForList().index(key, index);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 将list放入缓存
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lSet(String key, Object value) {
		try {
			redisTemplate.opsForList().rightPush(key, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将list放入缓存
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lSet(String key, Object value, long time) {
		try {
			redisTemplate.opsForList().rightPush(key, value);
			if (time > 0)
				expire(key, time);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将list放入缓存
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lSet(String key, List<Object> value) {
		try {
			redisTemplate.opsForList().rightPushAll(key, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将list放入缓存
	 * 
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lSet(String key, List<Object> value, long time) {
		try {
			redisTemplate.opsForList().rightPushAll(key, value);
			if (time > 0)
				expire(key, time);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 根据索引修改list中的某条数据
	 * @param key 键
	 * @param index 索引
	 * @param value 值
	 * @return
	 */
	public boolean lUpdateIndex(String key, long index, Object value) {
		try {
			redisTemplate.opsForList().set(key, index, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 移除N个值为value
	 * @param key 键
	 * @param count 移除多少个
	 * @param value 值
	 * @return 移除的个数
	 */
	public long lRemove(String key, long count, Object value) {
		try {
			Long remove = redisTemplate.opsForList().remove(key, count, value);
			return remove;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}
}

10、Redi配置文件详解

1、引入其他配置文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-huU79rmZ-1660633441041)(C:\Users\请叫我鹏鹏君\AppData\Roaming\Typora\typora-user-images\image-20220315114513976.png)]

2、网络
 bind 0.0.0.0 #绑定的IP
 protected-mode no #保护模式,默认yes
 port 6379 #默认端口
 
3、进程GENERAL
daemonize yes #是否允许后台进程允许,默认no,我们需要自己开启为yes
pidfile /var/run/redis_6379.pid #如果我们允许后台运行,我们需要指定一个pid文件
#日志
loglevel notice #默认日志方式
logfile "" #日志文件位置名
databases 16 #数据库的数量,默认是16个
always-show-logo no #是否总是显示logo

4、快照

持久化,在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb .aof

redis 是内存数据库,如果没有持久化,那么数据断电及失!

#如果在3600s内,如果至少有一个1 key进行了修改,我们及进行持久化操作
# save 3600 1
#如果在300s内,如果至少有一个10 key进行了修改,我们及进行持久化操作
# save 300 100
#如果在60s内,如果至少有一个10000 key进行了修改,我们及进行持久化操作
# save 60 10000

stop-writes-on-bgsave-error yes #持久化如果出错,是否要继续工作
rdbcompression yes #是否压缩rdb文件,需要消耗cpu资源
rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验
dir ./ #rdb文件保存的目录
5、SECURITY安全

redis默认没有密码

配置文件设置密码,手动加上一行

requirepass 123456
6、CLIENTS限制
# maxclients 10000 设置能连接上redis的最大客户端的数量
# maxmemory <bytes> redis配置最大的内存容量
maxmemory-policy noeviction #内存达到上限之后的处理策略
7、APPEND ONLY MODE 模式 aof配置
appendonly no #默认是不开启aof模式,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof" #持久化文件的名字

11、Redis订阅发布

1、简介

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、 微博、关注系统! Redis 客户端可以订阅任意数量的频道。 订阅/发布消息图: 第一个:消息发送者, 第二个:频道 第三个:消息订阅者!

第一个消息发送者,第二个频道,第三个消息订阅者

2、常用命令
  • PSUBSCRIBE pattern [pattern..] 订阅一个或多个符合给定模式的频道。
  • PUNSUBSCRIBE pattern [pattern..] 退订一个或多个符合给定模式的频道。
  • PUBSUB subcommand [argument[argument]] 查看订阅与发布系统状态。
  • PUBLISH channel message 向指定频道发布消息
  • SUBSCRIBE channel [channel..] 订阅给定的一个或多个频道。
  • UNSUBSCRIBE channel [channel..] 退订一个或多个频道
3、测试

订阅端

#订阅端
127.0.0.1:6379> SUBSCRIBE lcp #订阅一个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "lcp"
3) (integer) 1
#等待读取的消息
1) "message" #消息
2) "lcp" 	#那个频道的信息
3) "hello redis" #频道的具体消息

1) "message"
2) "lcp"
3) "springboot-redis"

发布端

127.0.0.1:6379> PUBLISH lcp 'hello redis' #发布者发布信息到频道
(integer) 1
127.0.0.1:6379> publish lcp 'springboot-redis' #发布者发布信息到频道
(integer) 1
4、原理

Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。

Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。

每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-88tklkho-1660633441043)(D:\LCP\桌面\Redis\2020051321554964.png)]

客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。

缺点

  1. 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
  2. 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。

应用

  1. 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
  2. 多人在线聊天室。

稍微复杂的场景,我们就会使用消息中间件MQ处理。

12、Redis集群环境搭建

1、为什么使用集群

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:

1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;

2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。

对于这种场景,我们可以使如下这种架构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x9uWLgNX-1660633441044)(D:\LCP\桌面\Redis\a.jpg)]

主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用! 一主二从!

只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!

总结

  1. 单台服务器难以负载大量的请求
  2. 单台服务器故障率高,系统崩坏概率大
  3. 单台服务器内存容量有限。
2、环境搭建

默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了!

认老大!一主(79)二从(80,81)

只要配置从库,不需要配置主库

1、复制3个配置文件,然后修改对应的信息
  • 端口
  • pid名字
  • log文件名字
  • dump.rdb名字
2、认老大,一主二从
#从机
127.0.0.1:6381> SLAveof 127.0.0.1 6380
OK
127.0.0.1:6381> info replication #找谁当老大
# Replication
role:slave #当前角色 
master_host:127.0.0.1 #主机信息
master_port:6380 #谁是主机
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:12106
slave_repl_offset:12106
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:22296357d4605d7d9885799db1a4639e37bd9436
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:12106
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:12079
repl_backlog_histlen:28

#主机
127.0.0.1:6380> info replication
# Replication
role:master #角色主机
connected_slaves:1  #从机数量
slave0:ip=127.0.0.1,port=6381,state=online,offset=12274,lag=0 #从机配置信息
master_failover_state:no-failover
master_replid:22296357d4605d7d9885799db1a4639e37bd9436
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:12274
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:12274

真实的从主配置应该在配置文件中配置,这样的话永久的,我们这里使用的是命令,暂时的

细节

主机可以写,从机不能写,只能读!主机中的所有信息和数据,都会自动被从机保存!

从机只能读取内容

#从机写操作报错
127.0.0.1:6381> set a2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6381> 

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

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

3、复制原理

Slave 启动成功连接到 master 后会发送一个sync同步命令

Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步

全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步

但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中看到!

4、宕机后手机配置主机

谋权篡位

如果主机断开了连接,我们可以使用SLAVEOF no one让自己手动变成主机!其他的节点就可以手动连接到最新的这个主机了!如果这个时候老大修复了,那就重新连接!

5、总结

1.从机只能读,不能写,主机可读可写但是多用于写。

2.当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。

3.当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。

4.第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:

  • 从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机
  • 使用哨兵模式(自动选举)

如果没有老大了,这个时候能不能选择出来一个老大呢?手动!

可以,那下面就来介绍哨兵模式

13、哨兵模式详解

(自动选举老大模式)

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

哨兵的作用:

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yss5Vzjx-1660633441046)(D:\LCP\桌面\Redis\狂神说Redis笔记09.jpg)]

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

哨兵模式优缺点

优点:

  1. 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有
  2. 主从可以切换,故障可以转移,系统的可用性更好
  3. 哨兵模式是主从模式的升级,手动到自动,更加健壮 缺点:
  4. Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
  5. 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项

14、缓存雪崩、击穿、穿透

1、缓存雪崩

什么是缓存雪崩?

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库,导致数据库压力激增,从而崩溃。

导致缓存雪崩的两个原因:

①缓存中有大量数据采用了相同的过期时间,从而同时过期,导致大量请求无法在Redis命中

解决方案:给这些数据的过期时间增加一个较小的随机数

②Redis 缓存实例发生故障宕机了,无法处理请求,这就会导致大量请求一下子积压到数据库层,从而发生缓存雪崩。

方案一:开启限流

方案二:针对热点数据,可以做多级缓存,可以用Guava在本地构建一个缓存,但需要考虑内存因素

第二个问题在生产环境中很少遇到,我们线上环境采用的是Redis集群架构,并且每个主节点都配备了两个从节点,而且服务器都分散在全国各地,几乎不会出现大面积Redis故障。

2、缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从数据库读取数据并设置到缓存,这个时候大并发的请求可能会瞬间把数据库压垮。

解决方案:采用互斥锁/分布式锁,让一个线程去查询就行,其他线程等待。

3、缓存穿透

什么是缓存穿透?

恶意请求缓存中不存在的数据,这导致缓存无法命中,每次请求都会查数据库(穿透到后端数据库进行查询),这个时候就出现穿透问题。

解决方案:

①如果数据库查不到,那缓存就设置null,并设置过期时间

②使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。

③假设我们用户ID是有规律的(例如长度是20),当请求过来,我们先判断这个ID是否符合我们的规律,如果不符合可以直接拦截(例如传过来的ID只有18位)
可读可写但是多用于写。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值