Nosql概述
为什么要用Nosql
用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长
什么是NoSQL
NoSQL
NoSQL = Not Only SQL
泛指非关系型数据库的,随着web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区!暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是必须要掌握的
用户的个人信息,社交网络,地理位置等数据的存储不需要一个固定的格式!不需要多余的操作就可以横向扩展的
NoSQL特点
解耦!
1、方便扩展(数据之间没有关系,很好扩展)
2、大数据是高性能(Redis一秒写8万次,读取11万,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高)
3、数据类型是多样型的!
4、传统RDBMS和NoSQL
NoSQL的四大分类
KV键值对
- 新浪:Redis
- 美团:Redis + Tair
- 阿里、百度: Redis + memecache
文档型数据库(bson格式和json一样)
- MongDB(一般必须要掌握)
- MongDB是一个基于分布式文件系统存储的数据库,c++编写
- 介于关系型数据库和非关系型数据库中间的产品
- ConthDB
列存储数据库
图形数据库
Redis入门
Redis安装
- 官网下载安装包
- 用xftp上传到服务器上并解压
- 安装gcc环境,用make指令安装相关依赖
- 在/usr/local/bin目录下创建aconfig文件夹并传入redis文件中的redis.conf
- 执行redis-server aconfig/redis.conf启动redis服务
- 执行redis-cli连接redis客户端
测试性能
#测试:100个并发连接 100000次请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
基础知识
redis默认有16个数据库
默认使用的是第0个
可以使用select进行切换数据库
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> DBSIZE
(integer) 0
127.0.0.1:6379[3]> set name acyang
OK
127.0.0.1:6379[3]> select 7
OK
127.0.0.1:6379[7]> get name
(nil)
127.0.0.1:6379[7]> select 3
OK
127.0.0.1:6379[3]> get name
"acyang"
清除当前数据库 flushdb
清空全部数据库flushall
127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]>
127.0.0.1:6379[3]> get name
(nil)
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> get name
"acyang"
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> flushall
OK
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> get name
(nil)
Redis是单线程的
明白Redis是很卡还的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!
Redis是C语言写的,官方提供的数据为100000+的QPS,完全不比同样是使用key-value的Memecache查!
Redis为什么单线程还这么快?
1、误区1:高性能的服务器一定是多线程的?
2、误区2:多线程(CUP上下文会切换!)一定比单线程效率高
核心:redis是将所有的数据全部放在内存中的,对于内存系统来说,如果没有上下文切换效率就是最高的
五大数据类型
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
Redis-Key
127.0.0.1:6379> set name acyang #设置 key
OK
127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> key *
(error) ERR unknown command 'key', with args beginning with: '*'
127.0.0.1:6379> keys * #查看所有的key
1) "age"
2) "name"
127.0.0.1:6379> EXISTS name #查看key是否存在
(integer) 1
127.0.0.1:6379> EXISTS names
(integer) 0
127.0.0.1:6379> move name 1 #移除key到另一个数据库
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) "name"
127.0.0.1:6379[1]> EXPIRE name 10 #设置过期时间
(integer) 1
127.0.0.1:6379[1]> ttl name #查看key还有多久过期
(integer) 2
127.0.0.1:6379[1]> keys *
(empty array)
String
127.0.0.1:6379> set key1 v1 # 设置值
OK
127.0.0.1:6379> APPEND key1 "hello" #追加字符串
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> STRLEN key1 # 返回长度
(integer) 7
127.0.0.1:6379>
127.0.0.1:6379> APPEND name "acyang" #直接创建新的key
(integer) 6
127.0.0.1:6379> get name
"acyang"
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # 自增1
(integer) 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
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> INCRBY views 10 # 自增指定步长
(integer) 10
127.0.0.1:6379> DECYBY views 10 # 自减指定步长
(error) ERR unknown command 'DECYBY', with args beginning with: 'views' '10'
127.0.0.1:6379> DECRBY views 10
(integer) 0
127.0.0.1:6379> getrange key1 0 3 #截取字符串
"v1he"
127.0.0.1:6379> getrange key1 0 -1 #0~-1即为获取全部的字符串
"v1hello"
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> setrange key2 1 xx #替换指定位置的字符串
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"
127.0.0.1:6379> setex key3 30 "hello" #创建key时同时设置过期时间
OK
127.0.0.1:6379> ttl key3
(integer) 26
127.0.0.1:6379> get key3
(nil)
127.0.0.1:6379> set mykey abc
OK
127.0.0.1:6379> setnx mykey dfd #不存在再创建Key
(integer) 0
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #设置多个键
127.0.0.1:6379> mget k1 k2 k3 #获取多个键的值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #msetnx是原子性的操作,要么一起成功,要么一起失败
(integer) 0
127.0.0.1:6379> mset user:1:name acyang user:1:age 5 #设置对象
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "acyang"
2) "5"
127.0.0.1:6379> getset db redis #如果不存在值,则返回nil,如果存在,则返回值,且给key赋上给定值
(nil)
127.0.0.1:6379> get db
"redis"
List
在redis里面,我们可以把List完成栈、堆、阻塞队列
127.0.0.1:6379> lpush list one #从头部插入一个值
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #遍历list
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1
1) "three"
2) "two"
127.0.0.1:6379> rpush list right #从尾部插入一个值
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> Lpop list #移除list的第一个值
"three"
127.0.0.1:6379> rpop list #移除list的最后一个值
"right"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1 #通过下标获取值
"one"
127.0.0.1:6379> lindex list j0
(error) ERR value is not an integer or out of range
127.0.0.1:6379> lindex list 0
"two"
127.0.0.1:6379> llen list #返回列表的长度
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lrem list 1 one #移除List中指定个数的元素
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "two"
127.0.0.1:6379> rpush mylist "hello1"
(integer) 1
127.0.0.1:6379> rpush mylist "hello2"
(integer) 2
127.0.0.1:6379> rpush mylist "hello3"
(integer) 3
127.0.0.1:6379> rpush mylist "hello4"
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2 #通过下标截取指定的长度
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello3"
###########################################
127.0.0.1:6379> rpush mylist "hello1"
(integer) 1
127.0.0.1:6379> rpush mylist "hello2"
(integer) 2
127.0.0.1:6379> rpush mylist "hello3"
(integer) 3
127.0.0.1:6379> rpush mylist "hello4"
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> rpoplpush myslist myotherlist
(nil)
127.0.0.1:6379> rpoplpush mylist otherlist #移除列表的最后一个元素,将他移动到新的列表中
"hello4"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
127.0.0.1:6379> lrange otherlist 0 -1
1) "hello4"
###########################################
Set(集合)
set中的值是不能重复的
#########################################
127.0.0.1:6379> sadd myset "hello" #set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset "acyang"
(integer) 1
127.0.0.1:6379> smembers myset #查看指定set的所有值
1) "hello"
2) "acyang"
127.0.0.1:6379> smember myset hello
(error) ERR unknown command 'smember', with args beginning with: 'myset' 'hello'
127.0.0.1:6379> sismember myset hello #判断某一个值是不是在set集合中
(integer) 1
######################################
127.0.0.1:6379> scard myset #获取set集合中的元素个数
(integer) 2
127.0.0.1:6379> srem myset "hello" #移除set中的元素
(integer) 1
127.0.0.1:6379> smembers myset
1) "acyang"
127.0.0.1:6379> SRANDMEMBER myset #随机获取myset中的一个元素
"acyang"
127.0.0.1:6379> SADD myset "yjc"
(integer) 1
127.0.0.1:6379> sadd myset "yj"
(integer) 1
127.0.0.1:6379> SRANDMEMBER myset 2 #随机获取myset中的两个个元素
1) "acyang"
2) "yj"
###########################################
127.0.0.1:6379> spop myset #随机删除set中的元素
"yj"
127.0.0.1:6379> spop myset
"yjc"
127.0.0.1:6379> SMEMBERS myset
1) "acyang"
127.0.0.1:6379> sadd myset i
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "i"
127.0.0.1:6379> sadd myset love
(integer) 1
127.0.0.1:6379> sadd myset you
(integer) 1
127.0.0.1:6379> sadd myset baby
(integer) 1
127.0.0.1:6379> sadd myset2 yes
(integer) 1
127.0.0.1:6379> smove myset myset2 baby #移动指定元素到另一个set中
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "i"
2) "love"
3) "you"
127.0.0.1:6379> SMEMBERS myset2
1) "yes"
2) "baby"
Hash(哈希)
Map集合,key-map 这个值是一个map集合。本质和String类型没有太大区别,还是一个简单的key-value
127.0.0.1:6379> hset myhash field1 acyang #添加hash中的元素
(integer) 1
127.0.0.1:6379> hget myhash field1
"acyang"
127.0.0.1:6379> hmset myhash field1 hello field2 world #设置多个元素
OK
127.0.0.1:6379> hmget myhash field1 field2 #获取多个key的value
1) "hello"
2) "world"
127.0.0.1:6379> hgetall myhash #获取全部的键值对
1) "field1"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> hdel myhash field1 #删除键值对
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
##############################################
127.0.0.1:6379> hlen myhash #获取hash表的长度
(integer) 1
127.0.0.1:6379> HEXISTS myhash field2 #获取hash中的字段是否存在
(integer) 1
127.0.0.1:6379> hkeys myhash #获取所有的key
1) "field2"
127.0.0.1:6379> HVALS myhash #获取所有的value
1) "world"
127.0.0.1:6379> hset myhash field1 4
(integer) 1
127.0.0.1:6379> HINCRBY myhash field1 1 #指定增量
(integer) 5
127.0.0.1:6379> hsetnx myhash field1 343 #如果存在则不能设置,如果不存在可以设置
(integer) 0
127.0.0.1:6379> hsetnx myhash field3 23
(integer) 1
hash变更的数据user name age,尤其是用户信息之类的,Hash更适合对象的存储
Zset(有序集合)
在set的基础上,增加了一个值,set k1 vq1 zset k1 score1 v1
127.0.0.1:6379> zadd myset 1 one #添加元素
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three
(integer) 2
127.0.0.1:6379> zrange myset 0 -1 #遍历元素
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zadd salary 50000 acyang
(integer) 1
127.0.0.1:6379> zadd salary 10000 lihua
(integer) 1
127.0.0.1:6379> zadd salary 8000 xw
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf #从小到大排序
1) "xw"
2) "lihua"
3) "acyang"
127.0.0.1:6379> ZRANGEBYSCORE salary 20000 +inf withscores
1) "acyang"
2) "50000"
127.0.0.1:6379> ZREVRANGE salary 0 -1 #逆向输出元素
1) "acyang"
2) "lihua"
3) "xw"
##############################
127.0.0.1:6379> zrem salary xw #移除指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1 #
1) "lihua"
2) "acyang"
127.0.0.1:6379> zcard salary #获取元素个数
(integer) 2
三种特殊数据类型
geospatial地理位置
朋友的定位,附件的人,打车距离计算
Redis的Geo在Redis3.2版本就推出了
getadd
# getadd无法添加北极南极
127.0.0.1:6379> geoadd china:city 106.639554 30.461747 guangan
(integer) 1
127.0.0.1:6379> geoadd china:city 106.57 30.47 chengdu
(integer) 1
getpos 获取指定元素的经纬度
127.0.0.1:6379> geopos china:city guangan
1) 1) "106.6395530104637146"
2) "30.4617478688526333"
geodist
两人之间的距离
单位:
- mi表示单位为英里
- ft表示单位为英尺
127.0.0.1:6379> geodist china:city chengdu guangan km
"6.7311"
georadius 以给定的经度纬度为中心,找出某一半径的元素
我附近的人?(获得所有附件人的地址、定位)通过半径来查询
127.0.0.1:6379> georadius china:city 110 30 1000 km #以100、30这个经纬度为中心,寻找方圆1000km内的城市
1) "chengdu"
2) "guangan"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist #显示到中心距离的位置
1) 1) "chengdu"
2) "333.7147"
2) 1) "guangan"
2) "326.9838"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord #显示他人的定位信息
1) 1) "chengdu"
2) 1) "106.56999796628952026"
2) "30.47000092094743451"
2) 1) "guangan"
2) 1) "106.6395530104637146"
2) "30.4617478688526333"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist withcoord count 1 #筛选出指定的结果
1) 1) "guangan"
2) "326.9838"
3) 1) "106.6395530104637146"
2) "30.4617478688526333"
127.0.0.1:6379>
georadiusbymember 找出位于指定元素周围的其他元素
geohash 返回一个或多个位置元素的Geohash表示
该命令将返回11个字符的Geohash字符串
GEO 底层的实现原理其实就是Zset !我们可以用zset操作geo
127.0.0.1:6379> zrange china:city 0 -1 #查看地图中全部的元素
1) "chengdu"
2) "guangan"
3) "beijin"
127.0.0.1:6379> zrem china:city chengdu #移除地图中指定的城市
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "guangan"
2) "beijin"
Hyperloglog
什么是基数?
基数:不重复的元素
简介
Redis2.8.9版本就更新了Hyperloglog数据结构
RedisHyperloglog基数统计的算法
优点:占用的内存是固定的,2^64不同的元素,只需要12KB内存,如果从内存角度来比较的话Hyperloglog首选
测试使用
127.0.0.1:6379> clear
127.0.0.1:6379> pfadd mykey a b c d e f g h i j # 创建第一组元素
(integer) 1
127.0.0.1:6379> pfadd mykey2 k l m n o p q
(integer) 1
127.0.0.1:6379> pfcount mykey # 查看数量
(integer) 10
127.0.0.1:6379> pfcount mykey2
(integer) 7
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 # 合并第一组和第二组元素到mykey3
OK
127.0.0.1:6379> pfcount mykey3
(integer) 17
如果允许容错,那么一定可以使用Hyperloglog
如果不允许容错,就使用set或者自己的数据类型
Bitmaps
位存储
统计用户信息,活跃,不活跃!登录、未登录!打卡,365打卡!两个状态的,都可以用Bitmaps!
Bitmaps位图,数据结构,都是操作二进制位来进行记录,就只有0和1两个状态
例如:使用bitmap来记录周一到周日的打卡
周一 : 1 周二 : 0 周三:0 周四:1…
127.0.0.1:6379> clear
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> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 6
(integer) 0
事务
要么同时成功,要么同时失败,原子性!
Redis单条命令是保存原子性的,但是事务不保证原子性
Redis事务本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行
一次性、顺序性、排他性!执行一系列的命令
Redsi事务没有隔离级别的概念
所有的命令在事务中,并没有被执行,只有发起执行命令的时候才会执行
Redis的事务:
-
开启事务(multi)
-
命令入队()
-
执行事务(exec)
正常执行事务!
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) "v2"
4) OK
放弃事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> discard #取消事务
OK
127.0.0.1:6379> get k4
(nil)
编译型异常,事务中的所有命令都不会执行
运行时异常,事务中的其他命令可以执行
监控!Watch
悲观锁:
- 很悲观,什么时候都会出问题,无论做什么都会加锁
乐观锁:
-
很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
-
获取version
-
更新的时候比较version
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 对象
127.0.0.1:6379> multi #事务正常结束,数据期间没有发生任何变动
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch可以当做redis的乐观锁操作
127.0.0.1:6379> watch money #监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec #执行之前,另一个线程修改了我们的值,这个时候,就会导致事务执行失败
(nil)
如果发现事务执行失败,就先解锁,再次监视
Jedis
我们要使用Java来操作Redis
Jedis是Redis官方推荐的java连接工具。使用Java操作Redis的中间件。如果你要使用Java操作redis,那么一定要对jedis十分的熟悉
测试
-
引入依赖
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.1</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.43</version> </dependency> </dependencies>
测试连接
import redis.clients.jedis.Jedis;
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("8.137.146.85",6379);
System.out.println(jedis.ping());
}
}
输出:
PONG
Process finished with exit code 0
常用的API
String
List
Set
Hash
Zset
所有的Api命令都是前面学过的命令
import redis.clients.jedis.Jedis;
public class TestApi {
public static void main(String[] args) {
Jedis jedis = new Jedis("8.137.146.85",6379);
jedis.set("name","acyang");
jedis.set("age",String.valueOf(4));
System.out.println("name=" + jedis.get("name") + "\nage="+ jedis.get("age"));
}
}
输出:
name=acyang
age=4
Process finished with exit code 0
事务
import com.alibaba.fastjson2.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class TestTrancation {
public static void main(String[] args) {
Jedis jedis = new Jedis("8.137.146.85", 6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("name","acyang");
jsonObject.put("hello","world");
String rs = jsonObject.toString();
Transaction multi = jedis.multi();
try{
multi.set("user1",rs);
multi.exec();
}catch (Exception e){
multi.discard(); //放弃事务
e.printStackTrace();
}finally {
System.out.println(jedis.get("user1"));
jedis.close();
}
}
}
输出:
{"name":"acyang","hello":"world"}
Process finished with exit code 0
SpringBoot整合Redis
SpringBoot 操作数据:spring-data jpa jdbc mongodb redis
SpringData 也是和 SpringBoot 齐名的项目
说明 : 在 SpringBoot2.x 之后,原来使用的jedis被替换为了lettuce
jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池
lettuce : 采用 netty ,实例可以在多个线程中进行共享,不存在线程不安全的情况。可以减少线程数量
源码分析:
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 默认的 RedisTemplate 没有过多的设置, redis 对象都是需要序列化
// 两个泛型都是 object, object 的类型,我们使用需要强制转换 <String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
// 由于 String 是redis中最常使用的类型,所以说单独提出来了一个bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
整合测试一下
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置连接
spring.redis.host=127.0.0.1
spring.redis.port=6379
3、测试
package com.acyang.redis02springboot;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Resource
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("name","acyang");
System.out.println(redisTemplate.opsForValue().get("name"));
}
}
输出:
acyang
Process finished with exit code 0
Redis持久化
RDB
什么是RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存中
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模的数据的恢复,且对于数据恢复的完整性不是非常敏感,那么RDB方式要比AOF更加的高效。RDB的缺点是最后一次持久化的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置
rdb保存的文件是dump.rdb是在我们的配置文件redis.conf中
触发机制
1、save的规则满足的情况下,会自动触发rdb规则
2、执行flushall命令,也会触发我们的rdb规则
3、退出redis,也会产生rdb文件
备份就自动产生一个dump.rdb
如果恢复rdb文件
1、只需要将rdb文件放在我们reids启动目录就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据
2、查看需要存在的位置
优点:
1、适合大规模的数据恢复
2、对数据完整性要求不高
缺点:
1、需要一定的时间间隔进行操作!如果redis意外宕机了,这个最后一次修改数据就没有的了
2、fork进程的时候,会占用一定的内存空间
AOF
将我们的所有命令都记录下来,history,恢复的时候就把这个文件重新执行一遍
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
Aof保存的时候 appendonly.aof文件
默认是不开启的,我们需要手动配置redis.conf文件,我们只需要将appendonly改成yes
如果这个aof文件有错误,这个时候仁帝山是启动不起来的,我们需要修改这个aof文件
redis给我们提供了一个工具 redis-check-aof --fix
优点和缺点
优点:
1、每一次修改都同步,文件的完整性会更好
2、每秒同步一次,可能会丢失一秒的数据
缺点:
1、相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
2、Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化
Redis发布订阅
Redis发布订阅(pub/sub)是一种消息通信模式:发送者发送消息,订阅者接受消息,微信、微博、关注系统!Redis客户端可以订阅任意数量的频道
指令 | 描述 |
---|---|
subscribe | 用户订阅up主 |
public | up主发布消息 |
示例:
- 订阅者:
127.0.0.1:6379> subscribe acyang #订阅频道
1) "subscribe"
2) "acyang"
3) (integer) 1
1) "message" #消息
2) "acyang" #消息来源
3) "hello" #消息内容
-
发布者:
127.0.0.1:6379> publish acyang "hello" #发布消息 (integer) 1
原理
Redis是使用C实现的,通过分析Redis源码里的public.c文件,了解发布和订阅机制的底层实现,借此加深对Redis的理解
Redis通过PUBLISH、SUBSRIBE和PSUBSCRIBE等命令实现发布和订阅功能
通过subscrile命令订阅某频道后,redis-sever里维护了一个字典,字典的键就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channels的客户端,subscribe命令的关键,就是将客户端添加到给定channels的订阅链表中。
通过publish命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它维护的channel字典中查到记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者
Pub/Sub从字面上理解就是发布和订阅,在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所以订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊功能等
使用场景:
1、实时消息系统
2、实时聊天(频道当做聊天室,将信息回给所以人即可)
3、订阅、关注系统都是可以的
稍微复杂的场景我们就会使用到消息中间件MQ
Redis主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器,前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能有主节点到从节点。Master以写为主,Slave以读为主
默认情况下,每台Redis服务器都是主节点
环境配置
只配置从库,不用配置主库
127.0.0.1:6379> info replication
# Replication
role:master #角色 master
connected_slaves:0 # 没有从机
master_failover_state:no-failover
master_replid:0de33e586c51a3ad0e16bbd799d0a6b95dcdf5d6
master_replid2:a3bc565c025828d10920b5e077b09424442ec887
master_repl_offset:0
second_repl_offset:1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
复制3个配置文件,然后修改对应的信息
1、端口
2、pid名字
3、log文件名字
4、dump.rdb名字
配置主从复制
默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了
127.0.0.1:6380> slaveof 127.0.0.1 6379 # 配置自己的主机
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:14
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:6f927927d2592ef77b0615c769ab1798d79c3b01
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:0
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2 #此时主机有两个从机
slave0:ip=127.0.0.1,port=6380,state=online,offset=238,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=238,lag=1
master_failover_state:no-failover
master_replid:6f927927d2592ef77b0615c769ab1798d79c3b01
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:238
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:238
127.0.0.1:6379>
真实的主从配置实在配置文件中配置的,这样是永久的
细节
主机可以写,从机不能写只能读,主机的所有信息都能被从机保存
127.0.0.1:6379> set k1 acyang #主机写
OK
127.0.0.1:6379>
127.0.0.1:6380> get k1 #从机能读到主机的信息
"acyang"
127.0.0.1:6380> set k2 "hello" #从机不能写
(error) READONLY You can't write against a read only replica
测试:主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取都主机写的信息
如果是使用命令行,从机重启了会变成主机!只要变为从机,立就会从主机中获取值
复制原理
Slave启动成功后连接到master后会发送一个sync同步命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕后,master将传送整个数据文件到slave,并完成一次完全同步
全量复制:slave服务自爱接受到数据文件后,将其存盘并加载到内存中
增量复制:Mater继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
哨兵模式
(当主机宕机后自动选举老大的模式)
概述
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功之后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线
测试!
我们目前的状态是一主二从
1、配置哨兵配置文件sentinel.conf
# sentinel monitor 被监控的名称 host port
sentinel monitor myredis 127.0.0.1 6379 1
后面的这个数字1,代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会称为主机
2、启动哨兵
[root@iZ2vc6yxrmz25p2f2uwh3gZ aconfig]# redis-sentinel sentinel.conf
2586:X 22 Mar 2024 10:13:15.433 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
2586:X 22 Mar 2024 10:13:15.433 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2586:X 22 Mar 2024 10:13:15.433 * Redis version=7.2.4, bits=64, commit=00000000, modified=0, pid=2586, just started
2586:X 22 Mar 2024 10:13:15.433 * Configuration loaded
2586:X 22 Mar 2024 10:13:15.434 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 7.2.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2586
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
此时主机断开后,哨兵会投票选出一个从机当做新的主机(依靠投票算法)
2586:X 22 Mar 2024 10:17:14.615 # +failover-end master myredis 127.0.0.1 6 #故障转移
2586:X 22 Mar 2024 10:17:14.615 # +switch-master myredis 127.0.0.1 6379 12
2586:X 22 Mar 2024 10:17:14.615 * +slave slave 127.0.0.1:6380 127.0.0.1 63
2586:X 22 Mar 2024 10:17:14.615 * +slave slave 127.0.0.1:6379 127.0.0.1 63
2586:X 22 Mar 2024 10:17:14.618 * Sentinel new configuration saved on disk
2586:X 22 Mar 2024 10:17:44.623 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6381 #切换主机
哨兵模式
当主机连回来后,会作为新主机的从机,这就是哨兵模式的规则
优点:
1、哨兵集群,基于主从复制,所有的主从配置有点,它全有
2、主从可以切换,故障可以转移,系统的可用性就会更好
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点:
1、Redis不好在线扩容的,集群容量一旦达到上限,在线扩容就十分麻烦
2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择
Redis缓存穿透和雪崩
缓存穿透
背景:查不到数据
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有击中,于是向持久层数据库查询,发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层的数据库造成很大的压力,这时候就相当于出现了缓存穿透
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据库会从缓存中获取,保护了后端数据源
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响
接口限流
根据用户或者IP对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常IP列入黑名单
缓存击穿
背景:热点缓存过期,请求量太大
概述
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一点个进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞
当某个Key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力过大
解决方案
- 设置热点数据永不过期或过期时间比较长
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期
- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力
缓存雪崩
概述
缓存在统一时间大面积的失效,导致大量的请求直接落到了数据库上,对数据库造成了巨大的压力
另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上
解决办法
针对Redis服务不可用的情况:
1、采用Redis集群,避免单机出现问题整个缓存服务都没办法使用
2、限流,避免同时处理大量的请求
3、多级缓存,例如本地缓存+Redis缓存的组合,当Redis缓存出现问题时,还可以从本地缓存中获取到部分数据
针对缓存失效的情况:
1、设置不同的失效时间比如随机设置缓存的失效时间
2、缓存永不失效
3、缓存预热,也就是在程序启动后或运行过程中,主动将热点数据加载到缓存中