本节课不会过多演示具体代码的操作,更多的会注重概念与原理,比较适用于复习
nosql即not only sql,泛指“非关系型数据库”
nosql不依赖业务逻辑方式储存,而是以key—value的键值对形式存储,因此大大增加了数据库的拓展能力
需要事务支持
基于sql的结构化查询存储,处理复杂的关系,需要即席查询
在数据仓库领域有一个概念叫Ad hoc queries,中文一般翻译为“即席查询”。
即席查询是指那些用户在使用系统时,根据自己当时的需求定义的查询。
当分布式架构的情况下,一个用户的访问可能会被负载均衡到不同的服务器,那么不同的服务器产生的session是不同的,不同的web服务器中并不能发现之前web服务器保存的session信息,那么可能会造成访问失败等问题
所以需要解决分布式session的问题,而redis就是其中解决方案之一
session同步的原理是在同一个局域网里面通过发送广播来异步同步session的
缺点:
优点:
直接将信息存储在cookie中
cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息
缺点:
在前端不进行负载均衡的情况下
我们利用nginx的反向代理和负载均衡,之前是客户端会被分配到其中一台服务器进行处理
具体分配到哪台服务器进行处理还得看服务器的负载均衡算法(轮询、随机、ip-hash、权重等),但是我们可以基于nginx的ip-hash
策略,可以对客户端和服务器进行绑定
同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理
缺点:
优点:
将session放在redis中,服务器需要时向其读取
优点:
缺点:
push/pop
、 add/remove
及取交集并集
和差集
及更丰富的操作,而且这些操作都是原子性
的master-slave主从同步
redis-benchmark
:性能测试工具,可以在自己电脑运行,看看自己电脑性能如何。redis-check-aof
:修复有问题的AOF文件, rdb和aof后面讲。redis-check-dump
:修复有问题的dump.rdb文件。redis-sentinel
: Redis集群使用。redis-server
: Redis服务器启动命令。redis-cli
:客户端,操作入口通过修改redis.conf的daemonize配置,完成后台启动
daemonize no
改为
daemonize yes
redis默认有16个数据库,类似于数组下标,默认使用0号库,不同库之间的数据不共享
但是统一密码管理,即所有库的密码都相同
select dbid
来切换数据库,例select 1
表示切换到1号库dbsize
查看当前库的key的个数Redis是单线程+多路IO复用技术
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的, 但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的
keys *
:查看当前库中所有的keyexists key
:判断是否存在某个keytype key
:查看key的类型del key
:删除keyunlink key
:根据value选择非阻塞删除expire key 10
:给key设置10秒的过期时间ttl key
:查看key过期时间,-1表示永不过期,-2表示已经过期set key value
:为key设置valueget key
:获取key的valuestring是redis中最常见的类型
String类型是二级制安全的,这意味着redis的string可以包含任何数据,例如jpg图片或序列化的对象
一个string的value最大是512M
string的数据类型为简单的动态字符串
是可以修改的字符串,内存结构类似于ArrayList,采用预分配冗余空间来减少内存的频繁分配
set key value
:为key设置value,若存在则覆盖
set key value nx ex time
以setnx互斥锁形式设置key的值为value,同时设置过期时间为timeget key
:根据key获取value
append key value
:对key的value追加新value,返回值是value的总长度
strlen key
:获取value的长度
setnx key value
:当key不存在时才设置value
incr key
:将key中储存的数字原子性地自增1,如果为空,新增value为1。只能对数字操作
这种原子性的操作不会被线程调度机制打断,一旦执行则一定保证运行到结束
当对非数字操作时报错:(error) ERR value is not an integer or out of range
decr key
:将key中存储的数字原子性地自减1,如果为空,新增value为-1。只能对数字操作
mset key1 value1 key2 value2 ...
:同时设置一个或多个key-value对
mget key1 key2 key3 ...
:同时获取一个或多个key
msetnx key1 value1 key2 value2 ...
:当其中所有key都不存在时,同时设置一个或多个key-value,否则直接不设置
getrange key begin end
:根据key获取从begin下标开始到end的value,相当于substring,下标从0开始
setrange key begin value
:根据key,新value覆盖从begin开始后的值,返回值是value长度
setex key time value
:设置key和value,同时设定过期时间time,单位秒
getset key value
:根据key获得旧value同时设置新value
单键多值
List列表是简单的字符串列表,按照插入顺序排序
可以添加一个元素到列表的头部(左边)或者尾部(右边)
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
List的数据结构是快速链表quickList
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,即:压缩列表
ziplist压缩列表将所有的元素紧挨着一起存储,分配的是一块连续的内存。
Redis将链表和ziplist结合起来组成了quicklist。 也就是将多个ziplist使用双向指针串起来使用
这样既满足了快速的插入删除性能,又不会出现太大的空间冗余
如果是普通的链表的话,需要的附加指针空间太大,会比较浪费空间。
比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next
lpush / rpush key value1 value2 ...
:从左/右边插入多个value值lpop / rpop key
:从左 / 右边弹出一个value值,值在键在,值亡键亡rpoplpush key1 key2
:从key1列表右边拿一个value放入key2列表的左边lrange key begin end
:从左到右,按照索引下标从begin到end,从左到右获得元素
lindex key index
:从左到右,根据index索引获取valuellen key
:获得列表长度set对外提供的功能是一种类似于list的一个列表的功能,但set的特殊之处在于set是无序可以自动去重
当我们需要储存一个列表数据,但又不希望出现重复数据时,set是个很好的选择
set提供了一个用于判断某个元素是否存在的接口,这在list中是没有的
因为set底层是一个value为null的hash表,所以添加、删除、查找的复杂度都是O(1)
这和java的HashSet几乎一样
set的数据结构是dict字典,字典是用hash表实现的
因为java中的HashSet底层也是HashMap,hashmap的value指向同一个事先new好的object
而hashset的value就是hashmap的key,所以保证了不会重复,但是也会导致无序
redis中set的结构也类似,使用的hash结构,所有value指向同一个值
sadd key value1 value2 ...
:将多个元素添加进集合key中,已经存在的元素将被忽略smembers key
:取出key集合中所有的值sismember key value
:判断key集合中是否有该value值,有返回1,没有则返回0scard key
:返回key集合中元素的个数srem key value1 value2 ...
:删除集合中的某些元素spop key
:随机从集合中删除并返回一个元素srandmember key n
:随机从key集合中读取n个值,且不会被删除smove set1 set2 value
:将set1中的value转移至set2sinter set1 set2
:返回set1和set2集合中交集的元素sunion set1 set2
:返回set1和set2集合中并集的元素sdiff set1 set2
:返回set1和set2集合中差集的元素redis中的hash是一个键值对集合
hash是一个string类型的field,和value的一个映射表,hash特别适合存储对象
类似于java中的Map
通过 key(用户Id)+filed(属性标签)
就可以操作对应的属性数据了
既不需要重复储存数据,也不会带来序列化和并发修改控制的问题
redis中hash的结构有两种:ziplist压缩列表
和hashtable哈希表
当field-value长度较短且个数较少时,使用ziplist压缩列表
反之则使用hashtable哈希表
- 当哈希对象的所有键值对的键和值的字符串长度都小于64字节
- 并且保存的键值对数量小于512个时
使用压缩列表
hset key field value
:给key表中的field属性赋值valuehget key field
:从key表中取出属性fieldhmset key1 field1 value1 filed2 value2
:批量设置key表中的field-value值hexists key field
:查看key表中field属性是否存在,存在返回1,不存在返回0hkeys key
:列出key表中所有的field属性hvals key
:列出key表中所有的value值hincrby key field increment
:对key表中的属性filed的value运算,value+=increment
,可以为负数hsetnx key field value
:仅当key表中field属性值不存在时,将field的值设置为value从名字可以看出来,zset比set多了个自动排序的功能,也就是有序集合
zset也是一个没有重复元素的字符串集合
不同之处是有序集合的每个成员都关联了一个评分( score ),通过这个score来进行排序
这个评分( score )被用来按照从最低分到最高分的方式排序集合中的成员
集合的成员是唯一的,但是评分可以是重复了,而且这个score评分在我们插入数据时也要一起插入
因为元素是有序的,所以zset也可以很快的根据评分( score )或者次序( position )来获取一个范围的元素访问有序集合的中间元素也是非常快的,因此我们能够使用有序集合作为一个没有重复成员的智能排序列表
SortedSet(zset)是redis提供的一个非常特别的数据结构
一方面它有着java中Map
另一方面它又类似于TreeSet,内部元素会按照score元素进行排序,并可以得到每个元素的名次,还可以通过score的范围获取元素的列表
zset底层使用了两种数据结构
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。
对于有序集合的底层实现,可以用数组、平衡树、链表等。
那么怎么提高单链表的查找效率呢?对链表建立一级 索引
,每两个节点提取一个结点到上一级,被抽出来的这级叫做 索引
或 索引层
。
这种链表加多级索引的结构,就是跳表
zset采用的是跳表。跳表效率堪比红黑树,实现远比红黑树简单
当结点数量多的时候,这种添加索引的方式,会使查询效率提高的非常明显
其实这也是一个“空间换时间”的算法,通过向上提取索引增加了查找的效率
zadd key score1 value1 score2 value2 ...
:将多个value放入key有序集中,score表示value的评分zrange key begin end [withscores]
:返回有序集key中,从begin开始到end的元素
withscores
则会将score一并返回zrangebyscore key min max [withscores] [limit offset count]
:返回有序集key中,所有score介于min到max的成员,成员按score从小到大排序zrevrangbyscore key max min [withscores] [limit offset count]
:返回有序集key中,所有score介于min到max的成员,成员按score从大到小排序zincrby key increment value
:为元素的score加上增量,即score+=incrementzrem key value
:删除有序集key中为value的元素zcount key min max
:统计有序集key中,score介于min到max间的元素个数zrank key value
:返回value元素在有序集key中的排名可以参考博文https://blog.csdn.net/Jay_Chou345/article/details/122131827
里面我写了使用java数组对bitset的简单实现
redis提供了这个bitmaps的数据类型,可以实现对位的操作
向位中添加数据
setbit
向key中偏移量为offset的位置添加值为value
获取数据
getbit
获取key中偏移量为offset的数据
获取记录数量
bitcount
获取key中value被设置为1的数量
bitcount
当start和end存在时,表示从start位开始读取到end位,end=-1时表示读取到最后一个位,end=-2时表示读取到倒数第二个位
当用户访问时,我们可以根据用户的id当做偏移量,value设为1,表示该用户访问
但是很多应用的用户id 以一个指定数字(例如10000 )开头,直接将用户id和Bitmaps的偏移对应,势必会造成一定的浪费
通常的做法是每次做setbit操作时将用户id减去这个指定数字
在第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
例如我们想设置id为5的用户在2021-12-24这天访问了,那么我们可以执行
setbit user:2021-12-24 5 1
如果我们想知道在2021-12-24这天,id为10的用户有没有访问
返回0表示未访问过
127.0.0.1:6379> getbit user:2021-12-24 10
(integer) 0
位运算
bitop
将key1和key2进行位运算,具体操作看operation
算出后的结果存在res这个key中
举个栗子
假设我们运行下列语句
# 表示在12.24这天,id为1、3、5的用户访问了
setbit user:1224 1 1
setbit user:1224 3 1
setbit user:1224 5 1
# 表示在12.25这天,id为1、2的用户访问了
setbit user:1225 1 1
setbit user:1225 2 1
# 将12.24和25号访问了的用户进行与运算,结果存放在result中
bitop and result user:1224 user:1225
# 查看id为1的用户是否在这俩天都访问了系统
127.0.0.1:6379> getbit result 1
(integer) 1
# 查看id为2的用户是否在这俩天都访问了系统
127.0.0.1:6379> getbit result 2
(integer) 0
一个数据集中不可再重复元素的个数
个人理解为数据去重后的个数
例如数据集{1,3,5,7,5,7,8},它的基数就是5个,因为去重后数据剩下了{1,3,5,7,8}共5个
Redis HyperLogLog 是用来做基数统计的算法
HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、很小的
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB
内存,就可以计算接近 2^64
个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素
为什么不用Set?
因为hyperloglog更加省空间!
添加
pfadd
向key中添加一个或更多的value
添加了不重复的元素即成功返回1,添加了重复元素返回0
127.0.0.1:6379> pfadd test 1 2 3 4 5
(integer) 1
127.0.0.1:6379> pfadd test 1
(integer) 0
127.0.0.1:6379> pfadd test 6
(integer) 1
127.0.0.1:6379> pfadd test 6
(integer) 0
获取基数
pfcount
获取key中的基数
127.0.0.1:6379> pfcount test
(integer) 6
合并key
pfmerge
把key1和key2中的元素加入到result中
例如将test1中{1,2,3}和test2{3,4,5}加入到res
进行去重后结果为{1,2,3,4,5},所以结果为5
127.0.0.1:6379> pfadd test1 1 2 3
(integer) 1
127.0.0.1:6379> pfadd test2 3 4 5
(integer) 1
127.0.0.1:6379> pfmerge res test1 test2
OK
127.0.0.1:6379> pfcount res
(integer) 5
Redis 3.2 中增加了对GEO类型的支持。
GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度
redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作
例如朋友的定位,附近的人,两地之间的距离,方圆几里的人,都可以用此数据类型实现
添加
geoadd
为key设置经度、纬度和成员名称
可以一次性添加多个经度、纬度、名称
两极无法直接添加,一般会下载城市数据,直接通过Java 程序一次性导入
有效的经度从-180 度到180 度。有效的纬度从-85.05112878 度到85.05112878度
当坐标位置超出指定范围时,该命令将会返回一个错误
已经添加的数据,是无法再次往里面添加的
查询
geopos
从key中查询一个或多个member成员的经纬度
获取两个member之间的直线距离
geolist
如果没有指定单位,默认以 m
作为距离单位
找出半径内的元素
georadius
以给定的经纬度为中心,找出 半径radius
内的member
持久化配置我们下文谈到持久化时再写入,这里简述一下其他的配置
port 6379
:老生常谈的redis运行的端口,默认6379bind 127.0.0.1
:绑定本机的IP地址,对应的网卡IP地址protected-mode yes
:是否保护模式,yes时外网无法访问tcp-backlog 511
:设置tcp的backlog
timeout 0
:连接超时时间,单位秒,默认0表示永不超时tcp-keepalive 0
:检查心跳时间,检测是否客户端还在活跃状态,若不活跃则断开连接,默认0表示永不超时daemonize yes
:是否为后台运行的守护进程,默认yespidfile /xxx
:存放pid文件的位置,每个实例会产生不同的pid文件loglevel notice
:日志级别
debug
:详细信息verbose
:有用的信息notice
:生产环境中使用的默认级别warning
:重要的信息logfile ""
:日志输出路径databases 16
:16个数据库,默认有16个库,默认使用0号库requirepass foobared
:redis的密码,默认是注释掉的,即无密码,可以在配置中修改maxclients 10000
:客户端与redis的最大连接数
max number of clients reached
作为回应maxmemory
:redis能够使用内存的上限值。当内存达到上限,redis会进行移除数据
maxmemory-policy
指定,默认noeviction
级别,即永不过期直接报错maxmemory-policy noeviction
:达到内存上限后移除数据级别,默认noeviction
: 永不过期,返回错误
volatile-lru
:只对设置了过期时间的key进行LRU(默认值)allkeys-lru
: 删除lru算法的keyvolatile-random
:随机删除即将过期keyallkeys-random
:随机删除volatile-ttl
: 删除即将过期的noeviction
: 永不过期,返回错误redis-cli -h 138.138.138.138 -p 6379 -a password
如果害怕命令传输不安全,可以先不输入密码,登录后在auth password
redis-cli -h 138.138.138.138 -p 6379
auth password
Redis发布订阅 (pub/sub) 是一种消息通信模式:
Redis客户端可以订阅任意数量的频道
使用命令subscribe sub
表示订阅sub
频道
127.0.0.1:6379> SUBSCRIBE sub
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "sub"
3) (integer) 1
使用命令publish sub test
表示向sub
频道发布消息test
127.0.0.1:6379> PUBLISH sub test
(integer) 1
这时,订阅了sub频道的客户端会接收到消息
可以看出这时一个list列表
1) "message"
2) "sub"
3) "test"
redis中的事务是会将一系列命令放入一个队列中依次执行,执行期间不会被打断
Multi
命令开始,输入的命令都会依次进入命令队列中,但不会执行
discard
来放弃组队Exec
后,,Redis会将之前的命令队列中的命令依次执行执行multi开启组队,在组队阶段输入的指令会返回QUEUED
表示加入队列
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user1 user1
QUEUED
127.0.0.1:6379> set user2 user2
QUEUED
127.0.0.1:6379> get user2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "user2"
组队阶段执行discard会直接放弃组队,即队列中的指令不会被执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user3 user3
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get user3
(nil)
组队时若放入了某个错误指令,在之后exec执行时,整个队列都不会执行,直接取消操作并报错
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user1 user1
QUEUED
127.0.0.1:6379> get user1
QUEUED
127.0.0.1:6379> set error
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> get user1
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
思考:有没有可能是因为被回滚了,所以得出的结论是不会被执行呢?
执行时某个指令出现了错误,错误命令将不被执行,其他命令会依次执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user1 user1
QUEUED
127.0.0.1:6379> get user1
QUEUED
127.0.0.1:6379> incr user1
QUEUED
127.0.0.1:6379> get user1
QUEUED
127.0.0.1:6379> exec
1) OK
2) "user1"
3) (error) ERR value is not an integer or out of range
4) "user1"
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
因为悲观锁会操作前悲观地上锁,让其只能被一个调用者去操作,所以安全性很高,但是效率比较低
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制
乐观锁适用于多读的应用类型,这样可以提高吞吐量
Redis 就是利用这种compare and swap
机制实现事务的
在保证cas是原子性的前提下,乐观锁相比于悲观锁的效率比较高,因为没有上锁
在执行multi之前,先执行watch
,可以监视一个或多个key的版本号
如果在事务执行之前这些 key 被其他命令所改动,那么事务将被打断
注:watch监视的是版本号,而不是具体值,下面会进行验证
unwatch
顾名思义就是取消所有监视呗
不过在exec执行事务或者discard取消事务后,watch会被自动解除
先说结论:watch监视的是version且可以解决ABA问题
前置
我们先set balance 1000
设置balance为1000
然后我们开启两个客户端同时监听balance
客户端1号
先让balance+=100
,再让balance-=100
使得balance的值表面上未发生改变,来验证ABA问题是否会存在,以得出watch是监视的value还是version
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby balance 100
QUEUED
127.0.0.1:6379> get balance
QUEUED
127.0.0.1:6379> incrby balance -100
QUEUED
127.0.0.1:6379> exec
1) (integer) 1100
2) "1100"
3) (integer) 1000
客户端2号
watch了balance后,在事务中为balance自增
执行事务发现返回nil
证明事务未生效
证明watch监视的是version且可以解决ABA问题
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby balance 500
QUEUED
127.0.0.1:6379> get balance
QUEUED
127.0.0.1:6379> exec
(nil)
事务中的所有命令都会序列化、按顺序地执行。
事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。
我们先给redis添加前置条件
表示id为100001的商品共有100件
set sk:100001:qt 100
首先我们直接使用一个简单的购买逻辑实现业务
我们使用LinkedBlockingQueue
当做简易的jedis连接池
经过以上步骤则完成了购买业务,但是这样会存在问题
如果没有并发访问的情况下,这样是可以实现业务需求的
但是当并发时,这些操作不是原子操作,极大概率会存在一件商品多个用户购买,超卖问题
public class Seckill {
private static BlockingQueue<Jedis> poll =new LinkedBlockingQueue<>(10);
static {
for (int i = 0; i < 10; i++) {
try {
poll.put(new Jedis("localhost",6379));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static boolean seckill(String prodid,String userId) throws InterruptedException {
//阻塞队列中取出实例
Jedis jedis=poll.take();
//执行秒杀业务,没有上锁、没有事务、存在超卖问题
boolean flag = kill(jedis, prodid, userId);
//归还jedis
poll.put(jedis);
return flag;
}
/**
* 没有上锁、没有事务、存在超卖问题
* @param jedis
* @param prodid
* @param userId
* @return
*/
private static boolean kill(Jedis jedis,String prodid,String userId){
//库存key,value为库存剩余容量
String kcKey = "sk:"+prodid+":qt";
//已购名单key,value为用户id
String userKey = "sk:"+prodid+":user";
//获取库存剩余量
String kc = jedis.get(kcKey);
//库存为空,活动未开始
if(null==kc){
System.out.println(userId+"号用户购买失败,库存为空,活动未开始");
return false;
}
//查看是否还有多余库存
if(Integer.parseInt(kc)<=0){
System.out.println(userId+"号用户购买失败,商品已经售罄,库存量剩余0");
return false;
}
//查询同一userId是否重复购买
if(jedis.sismember(userKey,userId)){
System.out.println(userId+"用户已购买过商品");
return false;
}
//库存-1
jedis.decr(kcKey);
//添加入购买名单
jedis.sadd(userKey,userId);
System.out.println(userId+"号用户秒杀成功");
return true;
}
}
我们开启200个线程去并发访问
public class RedisStudyApplication {
public static void main(String[] args){
for (int i = 100001; i <=100200; i++) {
int finalI = i;
new Thread(()->{
try {
Seckill.seckill("100001", finalI +"");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
购买的用户有103个,库存还剩-3个,出现超卖问题了
127.0.0.1:6379> get sk:100001:qt
"-3"
127.0.0.1:6379> smembers sk:100001:user
...
100) "100197"
101) "100198"
102) "100199"
103) "100200"
首先最简单的:上锁
直接一个synchronized锁住class,保证不会超卖而且可以保证不出现库存遗留问题
public static synchronized boolean seckill(String prodid,String userId){
...
}
如果我们想使用redis的机制解决这个问题呢?
使用multi事务+CAS乐观锁!
监视key后,在CAS情况下,库存发生改变导致事务失效,即订单失效
但是CAS只管安全,但是不管是否卖完,有可能出现库存遗留问题
所以还需要使用while进行自旋操作,我这里是用的一直自旋直到成功或失败
其实也可以使用超时机制或自旋次数限制,这里不多做阐述
以下是代码实现
/**
* 使用watch监听key,然后multi开启事务,防止超卖
* 但是CAS存在库存遗留问题,即:监视key后,在CAS情况下,库存发生改变导致事务失效,即订单失效
* 也就是库存并没有被卖完!CAS只管安全,但是不管是否卖完
* 所以可以使用自旋操作
* @param jedis
* @param prodid
* @param userId
* @return
*/
private static boolean tKill(Jedis jedis,String prodid,String userId){
//库存key,value为库存剩余容量
String kcKey = "sk:"+prodid+":qt";
//已购名单key,value为用户id
String userKey = "sk:"+prodid+":user";
boolean flag=true;
while(flag){
//监听库存key
jedis.watch(kcKey);
//获取库存剩余量
String kc = jedis.get(kcKey);
//库存为空,活动未开始
if(null==kc){
System.out.println(userId+"号用户购买失败,库存为空,活动未开始");
return false;
}
//查看是否还有多余库存
if(Integer.parseInt(kc)<=0){
System.out.println(userId+"号用户购买失败,商品已经售罄,库存量剩余0");
return false;
}
//查询同一userId是否重复购买
if(jedis.sismember(userKey,userId)){
System.out.println(userId+"用户已购买过商品");
return false;
}
//引入multi
Transaction multi = jedis.multi();
//库存-1
multi.decr(kcKey);
//添加入购买名单
multi.sadd(userKey,userId);
//执行事务,获取结果
List<Object> result = multi.exec();
if(null==result||result.size()==0){
System.out.println(userId+"号用户秒杀失败");
}else{
System.out.println(userId+"号用户秒杀成功");
flag=false;
}
}
return true;
}
结果
解决了超卖问题,且没有库存遗留,全部售出
127.0.0.1:6379> get sk:100001:qt
"0"
127.0.0.1:6379> smembers sk:100001:user
95) "100195"
96) "100196"
97) "100197"
98) "100198"
99) "100199"
100) "100200"
Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。
当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的
RDB是Redis默认的持久化方式。
按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储
对应产生的数据文件为**dump.rdb
**,通过配置文件中的save参数
来定义快照的周期
( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
Fork的作用是复制一个与当前进程一样的进程。
新进程的所有数据、数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益
save:save时在主进程开始保存,服务直接阻塞。手动保存[不建议]
bgsave:Redis 会在后台以异步方式进行快照操作,快照同时还可以响应客户端请求
可以通过lastsave
命令获取最后一次成功执行快照的时间
在redis.conf配置文件中
配置项:save
表示在time
秒内有keyNum
个key发生变化时,开始执行rdb持久化
如果keyNum设置过大,当部分key改变但未达到阈值时,不会进行rdb持久化
如果期间redis服务意外宕机,则数据必定丢失
写时拷贝
技术,但是如果数据庞大时还是比较消耗性能。AOF是以日志形式来记录每个写操作(增量保存)
Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog
当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复
redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
读操作不记录
只允许追加文件,不允许修改文件
持久化策略
持久化策略通过更改配置项appendfsync 持久化策略名称
来修改
always
:始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好everysec
:每秒同步,每秒记入日志一次;如果宕机,本秒的数据可能丢失no
:不主动同步,把同步时机交给操作系统重写策略
AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
auto-aof-rewrite-percentage
auto-aof-rewrite-min-size
AOF默认是关闭状态
可以在redis.conf中配置文件中配置项appendonly yes
以开启AOF机制
AOF文件名称默认为**appendonly.aofs
**,保存路径同RDB文件的路径一致
AOF和RDB同时开启,系统默认取AOF的数据,因为AOF机制使得数据不会存在丢失
当一个应用对一台redis服务读写操作,redis服务的压力很大
所以可以引入主从复制,以读写分离模式,让应用对主节点进行写操作,从节点进行读操作
写入主节点时,将数据复制一份到各个从节点,保证数据一致
当一个从节点宕机后,能立马切换到其他从节点进行读取,容灾快速恢复
include /myredis/redis.conf
pidfile /var/run/redis._6379.pid
port 6379
dbfilename dump6379.rdb
6379端口就写6379,6380就改成6380就行,6381就改成6381就行
如果开启了AOF也需要对应指定上不同的AOF配置文件名称,上面是默认关闭AOF的情况下的配置
然后指令通过不同的conf配置文件启动服务即可
redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf
注:当前只是仅仅启动了3个redis服务,并没有配置主从关系
使用info replication
打印主从复制的相关信息
role:master角色为master主节点
connected_slaves:0连接的从节点数量为0个
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
使用slaveof
成为某个服务的从节点
ip和port为主节点master的ip地址与port端口号
配置完主从节点后使用info replication
可以查看连接的从节点及其状态
在master上写入数据,slave上也可以get到
在slave上写入数据会报错
证明redis连上主从节点后,内部自动实现了读写分离的操作
在master节点宕机后,slaveof no one
指令可以使当前slave节点成为master节点
但是这样还需要手动指令切换,不人性化而且不方便
之后会讲到哨兵模式,即自动化的切换为master
假设我们有master、slave1和slave2
当一台从节点slave2宕机后,master写入了一些新数据
此时slave1当然是有数据的,但slave2有没有呢?肯定是没有,因为已经宕机了
如果对slave2进行重启,可以用info replication
发现它又变回了master状态
得出结论:slave节点重启后会变为普通master状态,需要重新slaveof为从节点
例子中,slave2宕机了,但是它重新连成slave2从节点后,数据会与master与slave1中的一致,因为重新从master中读取出来了
得出结论:当从节点连接到master,会将master中所有的数据复制到自身中!
全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:Master将所有新收集到的修改命令依次传给slave,完成同步
监控master是否故障,若故障了则根据投票数自动将某个slave转为master
根据上文配置一主二从的基础上
在/myredis文件夹下新建sentinel.conf
文件(名字不能写错)
在文件中写入sentinel monitor mymaster 127.0.0.1 6379 1
表示监控的master是127.0.0.1 6379
1表示投票数,即当master宕机后,至少有1个哨兵投票同意的情况下才可以升级为新的master
启动哨兵模式
redis-sentinel /myredis/sentinel.conf
启动哨兵模式
我们假设master为6379,slave为6380与6381
当6379宕机后,哨兵选举6380成为了新的master
并将6379作为slave加到了6380master下
当6379重启后,使用info replication
发现它是slave状态
这么多的slave到底选举哪个成为新master呢?
1.优先级
可以在redis.conf中配置优先级
slave-priority 1
表示当前节点的优先级为1,值越小优先级越高
新版本中可能是叫replica-priority
2.偏移量
哪个slave的数据与master中数据一致率高,则偏移量大
3.run-id
redis每次生成服务时,有一个run-id,是随机的40位的
如果上述都分不出哪个更适合成为master时
就会选举run-id最小的节点成为新的master
主从复制且有哨兵模式情况下,master可能会选举新的出来,那么在代码中就不能将redis配置写死
以jedis的连接池为例,可以看到,连接的地址是redis哨兵的地址端口
缓存穿透是指用户查询的数据在数据库没有,在缓存中也没有
这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空
如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
相当于进行了两次无用的查询
这样请求就绕过缓存直接查数据库,导致了经常提的缓存命中率问题
最常见的则是采用布隆过滤器
bitmap
中, 一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力可以在接口层增加校验
也有一个更为简单粗暴的方法就是空结果缓存
也可以设置白名单
bitmaps
类型定义一个可以访问的名单,名单id作为bitmaps的偏移量每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问最后的办法就是人工监控
5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。
如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)
Bitmap:典型的就是哈希表
缺点是,Bitmap对于每个元素只能记录1 bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。
bitmap简单学习笔记https://blog.csdn.net/Jay_Chou345/article/details/122131827
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期)
某个key过期了,由于并发用户特别多,都在访问这个key的数据,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
缓存预热——预先设置热门数据:在redis被访问前,就提前将一些热门数据存入到redis中
实时监控哪些数据比较热门,延长过期时间,或设置**热点数据永远不过期 **
我们可以简单的理解为:由于原有缓存失效,新缓存未到期间造成了系统雪崩效应
例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期
所有原本应该访问缓存的请求都去查询数据库了,对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机
从而形成一系列连锁反应,造成整个系统崩溃
缓存击穿与缓存雪崩的区别
缓存击穿:指并发查同一条数据,并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据
缓存雪崩:是不同数据都过期了,很多数据都查不到从而查数据库,导致压力过大
大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免缓存失效时大量的并发请求落到底层存储系统上,性能不高,不适合高并发场景
构建多级缓存架构:nginx缓存、redis缓存、其他缓存(ehcache等)
设置过期提醒,记录key的过期时间,如果将要过期时,去触发其他线程去立马更新key的缓存
最简单方案就是将缓存失效时间分散开
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。
避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题
让用户可以直接查询事先被预热的缓存数据而无需检索数据库
直接写个缓存刷新页面,上线时手工操作下
数据量不大,可以在项目启动的时候自动进行加载
定时刷新缓存
这里小小地讲一下springboot
ApplicationRunner
接口类,它是一个函数型接口,只有一个void run(ApplicationArguments args)
方法用于在main方法启动后进行回调CommandLineRunner
接口类,它同样有一个void run(String... args)
方法,在main方法启动后进行回调执行具体内容可以查看我的博文《如何让SpringBoot项目启动时执行特定代码》
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
两者各有优劣
第一种的缺点是维护大量缓存的key是比较麻烦的
第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。
系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级
警告:有些服务在一段时间内 成功率有波动(如在95~ 100%之间),可以自动降级或人工降级,并发送告警
错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级
严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题
因此,对于不重要的缓存数据,可以采取服务降级策略
例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力
为了解决这个问题就需要一种跨jvm的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
实现分布式锁的主流方案:
setnx key value
:当key不存在时才设置value
但是这个有一个很严重的安全问题,就是持有锁之后一直不进行释放!
那么就可以设定一个过期时间防止到期不释放
expire key 10
:给key设置10秒的过期时间
因为事务不保证原子性,所以我们一条指令同时设置key和time才是最好的
set key value nx ex time
以setnx互斥锁形式设置key的值为value,同时设置过期时间为time
例如set user if nx ex 10
表示设置user的值为if,且10秒后过期
当A上锁后,业务执行非常缓慢,以至于超过了上锁的过期时间
然后A在执行业务时,B获取到锁也开始执行任务
当A执行完后,进行del时,由于key相同,会把B的锁一并删除
从而导致一系列的蝴蝶效应(雪崩)
那么解决这个问题的方法就是:保证每把锁的key不相同
那么不相同那肯定是用随机码,比如常见的uuid等
即:使用随机key以防止误删
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。
虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。
需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查
(如果每隔100ms,全部key进行检查,redis岂不是卡死)。
因此,如果只采用定期随机删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效
这样,redis的内存会越来越高。那么就应该采用内存淘汰机制
在redis.conf中
maxmemory
:redis能够使用内存的上限值。当内存达到上限,redis会进行移除数据
maxmemory-policy
指定,默认noeviction
级别,即永不过期直接报错maxmemory-policy noeviction
:达到内存上限后移除数据级别,默认noeviction
: 永不过期,返回错误
volatile-lru
:只对设置了过期时间的key进行LRU(默认值)allkeys-lru
: 删除lru算法的keyvolatile-random
:随机删除即将过期keyallkeys-random
:随机删除volatile-ttl
: 删除即将过期的noeviction
: 永不过期,返回错误