java晋级赛 Redis数据库

根据狂神的视频整理而得 https://www.bilibili.com/video/BV1S54y1R7SB?p=15&spm_id_from=pageDriver

一、Redis基础

中文网:http://www.redis.cn/
概况:
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件MQ。
它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合
(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询

Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

作用和特性:

  • 内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof)
  • 效率高,可以用于高速缓存
  • 发布订阅系统
  • 地图信息分析
  • 计时器、计数器(浏览量!)

扩展:
redis 默认有16个数据库
默认使用的是第0个
redis 是单线程的
为什么redis单线程还这么快
1.误区1:高新能的服务器一定是多线程的?
2.误区2:多线程(cpu上下文切换)一定比单线程效率高
先去cpu 内存 硬盘
redis 是将使用的数据全部放在内存中,使用单线程是最快的 ,没有上下文切换

redis常用命令:

 redis默认有 16 个数据库
默认使用的是第 0 个

可以使用 select 进行切换数据库!
select index # 0-16

清除当前数据库 
flushdb

清除全部数据库的内容 
FLUSHALL

//退出
quit
exit
//清屏
clear

keys *  # 查看数据库所有的key
127 .0.0.1:6379> keys *  # 查看所有的key
(empty list or set)
127 .0.0.1:6379> set name lisen  # set key
OK
127 .0.0.1:6379> keys *
1 ) "name"
127 .0.0.1:6379> set age 1
OK
127 .0.0.1:6379> keys *
1 ) "age"
2 ) "name"
127 .0.0.1:6379> EXISTS name  # 判断当前的key是否存在
(integer) 1
127 .0.0.1:6379> EXISTS name
(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> set name lisen
OK
127 .0.0.1:6379> keys *
1 ) "age"
2 ) "name"
127 .0.0.1:6379> clear
127 .0.0.1:6379> keys *
1 ) "age"
2 ) "name"
127 .0.0.1:6379> get name
"qinjiang"
127 .0.0.1:6379> EXPIRE name 10 #设置key的过期时间,单位是秒
(integer) 1
127 .0.0.1:6379> ttl name  # 查看当前key的剩余时间
(integer) 4
127 .0.0.1:6379> ttl name
(integer) 3
127 .0.0.1:6379> ttl name
(integer) 2
127 .0.0.1:6379> ttl name
(integer) 1
127 .0.0.1:6379> ttl name
(integer) -
127 .0.0.1:6379> get name # 就会自动删除 keys *也就没了
(nil)
127 .0.0.1:6379> type name  # 查看当前key的一个类型!string
127 .0.0.1:6379> type age   # string


二、数据类型

Redis所有的key均为String类型,讨论数据类型是说的value的类型

String(字符串)

基础用法

############################################################
127 .0.0.1:6379> set name lisen  # 设置值
127 .0.0.1:6379> get lisen # 获得值
127 .0.0.1:6379> keys * # 获得所有的key
127 .0.0.1:6379> EXISTS lisen  # 判断某一个key是否存在
(integer) 1
#追加字符串,如果当前key不存在,就相当于setkey
127 .0.0.1:6379> APPEND lisen "hehe"  # 9就是name的值的长度
(integer) 9
127 .0.0.1:6379> get lisen
"lisenhehe"
127 .0.0.1:6379> STRLEN key1  # 获取字符串的长度!
(integer) 9
############################################################
# i++
# 步长 i+=
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> incr views
(integer) 2
127 .0.0.1:6379> get viewsg
"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> decr views
(integer) -1
127 .0.0.1:6379> get views
"-1"
127 .0.0.1:6379> INCRBY views 10 # 可以设置步长,指定增量!
(integer) 9
127 .0.0.1:6379> INCRBY views 10
(integer) 19
127 .0.0.1:6379> DECRBY views 5
(integer) 14
############################################################

String也可以作为数值的操作,用 incr命令 可以实现类似于 计时器、计数器(比如说微信公众号的浏览数,微博的粉丝数等等)

//增长指令,只有当value为数字时才能增长
incr key  
incrby key increment  
incrbyfloat key increment 

//减少指令,有当value为数字时才能减少
decr key  
decrby key increment

字符串范围range

127.0.0.1:6379[13]> set key1 hello,lisen # 设置 key1 的值
OK
127.0.0.1:6379[13]> GETRANGE key1 0 3  # 截取字符串 [0,3]
"hell"
127.0.0.1:6379[13]> GETRANGE key1 0 -1 # 获取全部的字符串 和 get key是一样的
"hello,lisen"
127.0.0.1:6379[13]> SETRANGE key1 1 xx #替换指定位置开始的字符串!写多少长度的就换多少长度的
127.0.0.1:6379[13]> SETRANGE key1 0 l
(integer) 11
127.0.0.1:6379[13]> get key1
"lello,lisen"
127.0.0.1:6379[13]> SETRANGE key1 0 lllllll
(integer) 11
127.0.0.1:6379[13]> get key1
"lllllllisen"

设置set的过期时间

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

127.0.0.1:6379[13]> SETEX key1 10 lisen # key1 的值为 lisen,10秒后过期
OK
127 .0.0.1:6379[13]> setnx mykey "redis" # 如果mykey 不存在,创建mykey
# 同时设置多个值
# mset
127.0.0.1:6379[13]> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379[13]> keys *
1) "view"
2) "k3"
3) "k2"
4) "k1"

# 同时获取多个值
# mget
127 .0.0.1:6379> mget k1 k2 k3 
1 ) "v1"
2 ) "v2"
3 ) "v3"

List(列表)

数据存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分
需要的存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序
list类型:保存多个数据,底层使用双向链表存储结构实现
元素有序,且可重

基本操作

//添加修改数据,lpush为从左边添加,rpush为从右边添加
lpush key value1 value2 value3...
rpush key value1 value2 value3...

//获取并移除元素(从list左边或者右边移除)
lpop key
rpop key

//查看数据, 从左边开始向右查看. 如果不知道list有多少个元素,end的值可以为-1,代表倒数第一个元素
//lpush先进的元素放在最后,rpush先进的元素放在最前面
lrange key start end
//得到长度
llen key
//取出对应索引的元素
lindex key index

在redis里面,我们可以把list变成 栈、队列、阻塞队列
所有的list命令都是用l开头的,Redis不区分大小命令

进阶操作

##########################################################################
# LPUSH
# RPUSH
# 这边只能左边插入右边插入 但是取值只能LRANGE
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 righr  # 将一个值或者多个值,插入到列表位部 (右)
(integer) 4
127 .0.0.1:6379> LRANGE list 0 -1
1 ) "three"
2 ) "two"
3 ) "one"
4 ) "righr"
##########################################################################
# 双端队列的感觉
# LPOP
# RPOP
127 .0.0.1:6379> Lpop list  # 移除list的第一个元素 
"three"
127 .0.0.1:6379> Rpop list  # 移除list的最后一个元素
"righr"
127 .0.0.1:6379> LRANGE list 0 -1
1 ) "two"
2 ) "one"
##########################################################################
# Lindex
127 .0.0.1:6379> LRANGE list 0 -1
1 ) "two"
2 ) "one"
127 .0.0.1:6379> lindex list 1 # 通过下标获得 list 中的某一个值!
"one"
127 .0.0.1:6379> lindex list 0
"two"
##########################################################################
# Llen key  # list的长度
127 .0.0.1:6379> Llen list # 返回列表的长度
(integer) 3
##########################################################################
# 移除指定的值!
# lrem key 个数 具体的值 # 移除指定的值 可移除多个
127 .0.0.1:6379> lrem list 1 one # 移除list集合中指定个数的value,精确匹配
##########################################################################
# ltrim 修剪 没有rtrim
127.0.0.1:6379[13]> rpush mylist hello hello1 hello2 hello3
(integer) 4
127.0.0.1:6379[13]> LRANGE mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
# 通过下标截取指定的长度,这个list已经被改变了,截断了只剩下截取的元素!
127.0.0.1:6379[13]> LTRIM mylist 1 2
OK
127.0.0.1:6379[13]> LRANGE mylist 0 -1
1) "hello1"
2) "hello2"

##########################################################################
# 移除列表的最后一个元素,将他移动到新的列表中!
# RPOPLPUSH source destination
# rpoplpush list名称 newlist名称 
127.0.0.1:6379[13]> LRANGE mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379[13]> RPOPLPUSH mylist mylistnew
"hello3"
127.0.0.1:6379[13]> LRANGE mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
127.0.0.1:6379[13]> LRANGE mylistnew 0 -1
1) "hello3"

应用
消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)

Set(集合)

特点
不重复且无序

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。

基本操作

//添加元素
sadd key member1 member2...

//查看元素
smembers key

//移除元素
srem key member

//查看元素个数
scard key

//查看某个元素是否存在
sismember key member

因为set的特点不重复且无序,可以使用这个特性 来 做抽奖的功能;

进阶操作

---------------SADD--SCARD--SMEMBERS--SISMEMBER--------------------

127.0.0.1:6379> SADD myset m1 m2 m3 m4 # 向myset中增加成员 m1~m4
(integer) 4
127.0.0.1:6379> SCARD myset # 获取集合的成员数目
(integer) 4
127.0.0.1:6379> smembers myset # 获取集合中所有成员
1) "m4"
2) "m3"
3) "m2"
4) "m1"
127.0.0.1:6379> SISMEMBER myset m5 # 查询m5是否是myset的成员
(integer) 0 # 不是,返回0
127.0.0.1:6379> SISMEMBER myset m2
(integer) 1 # 是,返回1
127.0.0.1:6379> SISMEMBER myset m3
(integer) 1

---------------------SRANDMEMBER--SPOP----------------------------------

127.0.0.1:6379> SRANDMEMBER myset 3 # 随机返回3个成员
1) "m2"
2) "m3"
3) "m4"
127.0.0.1:6379> SRANDMEMBER myset # 随机返回1个成员
"m3"
127.0.0.1:6379> SPOP myset 2 # 随机移除并返回2个成员
1) "m1"
2) "m4"
# 将set还原到{m1,m2,m3,m4}

---------------------SMOVE--SREM----------------------------------------

127.0.0.1:6379> SMOVE myset newset m3 # 将myset中m3成员移动到newset集合
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "m4"
2) "m2"
3) "m1"
127.0.0.1:6379> SMEMBERS newset
1) "m3"
127.0.0.1:6379> SREM newset m3 # 从newset中移除m3元素
(integer) 1
127.0.0.1:6379> SMEMBERS newset
(empty list or set)

-----------------------------SDIFF------------------------------------
# 下面开始是多集合操作,多集合操作中若只有一个参数默认和自身进行运算
# setx=>{m1,m2,m4,m6}, sety=>{m2,m5,m6}, setz=>{m1,m3,m6}
127.0.0.1:6379> SDIFF setx sety setz # 等价于setx-sety-setz
1) "m4" #这就是看setx中与其他集合中有不一样的列出来 只针对setx中的列出来
127.0.0.1:6379> SDIFF setx sety # setx - sety
1) "m4"
2) "m1"
127.0.0.1:6379> SDIFF sety setx # sety - setx
1) "m5"

-------------------------SINTER---------------------------------------
# 共同关注(交集)

127.0.0.1:6379> SINTER setx sety setz # 求 setx、sety、setx的交集
1) "m6"
127.0.0.1:6379> SINTER setx sety # 求setx sety的交集
1) "m2"
2) "m6"

-------------------------SUNION---------------------------------------

127.0.0.1:6379> SUNION setx sety setz # setx sety setz的并集
1) "m4"
2) "m6"
3) "m3"
4) "m2"
5) "m1"
6) "m5"
127.0.0.1:6379> SUNION setx sety # setx sety 并集
1) "m4"
2) "m6"
3) "m2"
4) "m1"
5) "m5"

set 实际运用: 通过交集,并集 ,差集 可以实现类似于微博的共同好友等等:
微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!
例如共同关注,共同爱好,二度好友,推荐好友

Hash(哈希)

Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。(就是key-value 都是字段 一行数据)

基本操作

//插入(如果已存在同名的field,会被覆盖)
hset key field value
hmset key field1 value1 field2 value2...
//插入(如果已存在同名的field,不会被覆盖)
hsetnx key field value

//取出
hget key field
hgetall key

//删除
hdel key field1 field2...

//获取field数量
hlen key

//查看是否存在
hexists key field

//获取哈希表中所有的字段名或字段值 
hkeys key
hvals key

//设置指定字段的数值数据增加指定范围的值 
hincrby key field increment 
hdecrby key field increment

进阶操作

-----------------------HSET--HMSET--HSETNX----------------

127.0.0.1:6379> HSET studentx name sakura # 将studentx哈希表作为一个对象,设置name为sakura
(integer) 1
127.0.0.1:6379> HSET studentx name gyc # 重复设置field进行覆盖,并返回0
(integer) 0
127.0.0.1:6379> HSET studentx age 20 # 设置studentx的age为20
(integer) 1
127.0.0.1:6379> HMSET studentx sex 1 tel 15623667886 # 设置sex为1,tel为15623667886
OK
127.0.0.1:6379> HSETNX studentx name gyc # HSETNX 设置已存在的field
(integer) 0 # 失败
127.0.0.1:6379> HSETNX studentx email 12345@qq.com
(integer) 1 # 成功

----------------------HEXISTS--------------------------------
127.0.0.1:6379> HEXISTS studentx name # name字段在studentx中是否存在
(integer) 1 # 存在
127.0.0.1:6379> HEXISTS studentx addr
(integer) 0 # 不存在

-------------------HGET--HMGET--HGETALL-----------
127.0.0.1:6379> HGET studentx name # 获取studentx中name字段的value
"gyc"
127.0.0.1:6379> HMGET studentx name age tel # 获取studentx中name、age、tel字段的value
1) "gyc"
2) "20"
3) "15623667886"
127.0.0.1:6379> HGETALL studentx # 获取studentx中所有的field及其value
 1) "name"
 2) "gyc"
 3) "age"
 4) "20"
 5) "sex"
 6) "1"
 7) "tel"
 8) "15623667886"
 9) "email"
10) "12345@qq.com"


--------------------HKEYS--HLEN--HVALS--------------
127.0.0.1:6379> HKEYS studentx # 查看studentx中所有的field
1) "name"
2) "age"
3) "sex"
4) "tel"
5) "email"
127.0.0.1:6379> HLEN studentx # 查看studentx中的字段数量
(integer) 5
127.0.0.1:6379> HVALS studentx # 查看studentx中所有的value
1) "gyc"
2) "20"
3) "1"
4) "15623667886"
5) "12345@qq.com"

-------------------------HDEL--------------------------
127.0.0.1:6379> HDEL studentx sex tel # 删除studentx 中的sex、tel字段
(integer) 2
127.0.0.1:6379> HKEYS studentx
1) "name"
2) "age"
3) "email"

-------------HINCRBY--HINCRBYFLOAT------------------------
127.0.0.1:6379> HINCRBY studentx age 1 # studentx的age字段数值+1
(integer) 21
127.0.0.1:6379> HINCRBY studentx name 1 # 非整数字型字段不可用
(error) ERR hash value is not an integer
127.0.0.1:6379> HINCRBYFLOAT studentx weight 0.6 # weight字段增加0.6
"90.8"

hash 类型数据操作的注意事项
hash类型下的value只能存储字符串,不允许存储其他数据类型,不存在嵌套现象。如果数据未获取到, 对应的值为(nil)
每个 hash 可以存储 2^32 - 1 个键值
hash类型十分贴近对象的数据存储形式,并且可以灵活添加删除对象属性。但hash设计初衷不是为了存储大量对象而设计的,切记不可滥用,更不可以将hash作为对象列表使用
hgetall 操作可以获取全部属性,如果内部field过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈

hash的用法 这边提一下java中简单用法

# 这是fastjson中map转对象的
Map map1 = redisTemplate.opsForHash().entries(each);

RedisAccompanyRecord eachRecord = JSONObject.parseObject(JSONObject.toJSONString(map1), RedisAccompanyRecord.class);

# 这是fastjson中对象转map存储到redis中
Map<String,Object> map = JSONObject.parseObject(JSONObject.toJSONString(redisAccompanyRecord), Map.class);

 //redis的 key 标准 : ACT_流程id_机器id_人员id
redisTemplate.opsForHash().putAll("ACT_"+redisAccompanyRecord.getKey()+"_"+redisAccompanyRecord.getMachineId()+"_"+redisAccompanyRecord.getUserId(),map);

Zset(有序集合)

不重但有序(score)

新的存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式

需要的存储结构:新的存储模型,可以保存可排序的数据

sorted_set类型:在set的存储结构基础上添加可排序字段

基本操作

//插入元素, 需要指定score(用于排序)
zadd key score1 member1 score2 member2

//查看元素(score升序), 当末尾添加withscore时,会将元素的score一起打印出来
zrange key start end (withscore)
//查看元素(score降序), 当末尾添加withscore时,会将元素的score一起打印出来
zrevrange key start end (withscore)

//移除元素
zrem key member1 member2...

//按条件获取数据, 其中offset为索引开始位置,count为获取的数目
zrangebyscore key min max [withscore] [limit offset count]
zrevrangebyscore key max min [withscore] [limit offset count]

//按条件移除元素
zremrangebyrank key start end
zremrangebysocre key min max
//按照从大到小的顺序移除count个值
zpopmax key [count]
//按照从小到大的顺序移除count个值
zpopmin key [count]

//获得元素个数
zcard key

//获得元素在范围内的个数
zcount min max

//求交集、并集并放入destination中, 其中numkey1为要去交集或并集集合的数目
zinterstore destination numkeys key1 key2...
zunionstore destination numkeys key1 key2...

注意

min与max用于限定搜索查询的条件
start与stop用于限定查询范围,作用于索引,表示开始和结束索引
offset与count用于限定查询范围,作用于查询结果,表示开始位置和数据总量

拓展操作

//查看某个元素的索引(排名)
zrank key member
zrevrank key member

//查看某个元素索引的值
zscore key member
//增加某个元素索引的值
zincrby key increment member

注意事项

  • score保存的数据存储空间是64位,如果是整数范围是-9007199254740992~9007199254740992
  • score保存的数据也可以是一个双精度的double值,基于双精度浮点数的特征,可能会丢失精度,使用时候要慎重
  • sorted_set底层存储还是基于set结构的,因此数据不能重复,如果重复添加相同的数据,score值将被反复覆盖,保留最后一次修改的结果

案例思路:set 排序 存储班级成绩表,工资表排序!
普通消息,1, 重要消息 2,带权重进行判断!
排行榜应用实现,取Top N 测试!

三大特殊类型

Geospatial 地理位置

Redis 的 Geo 在Redis3.2 版本就推出了! 这个功能可以推算地理位置的信息,两地之间的距离,方圆
几里的人!
作用:可以实现功能的有:定位附近的人打车距离计算
官方文档:https://www.redis.net.cn/order/3685.html

getadd

#getadd 添加地理位置 # 规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!
#有效的经度从-180度到180度。 # 有效的纬度从-85.05112878度到85.05112878度。
#当坐标位置超出上述指定范围时,该命令将会返回一个错误。 
#127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin 
 (error) ERR invalid longitude,latitude pair 39.900000,116.400000 # 参数 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

getpos

获得当前定位:一定是一个坐标值!

 127.0.0.1:6379> GEOPOS china:city beijing # 获取指定的城市的经度和纬度! 
 1) 1) "116.39999896287918091" 
 2) "39.90000009167092543" 
 127.0.0.1:6379> GEOPOS china:city beijing chongqi 
 1) 1) "116.39999896287918091" 
 2) "39.90000009167092543" 
 2) 1) "106.49999767541885376" 
 2) "29.52999957900659211"

GEODIST
两人之间的距离

单位:

  • 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"

georadius
以给定的经纬度为中心, 找出某一半径内的元素
比如说我附近的人? (获得所有附近的人的地址,定位!)通过半径来查询!
获得指定数量的人,200
所有数据应该都录入:china:city ,才会让结果更加请求!

 127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 以11030 这个经纬度为中心,寻 
 找方圆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> GEORADIUS 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> GEORADIUS china:city 110 30 500 km withdist withcoord count 
 2 1) 1) "chongqi" 
 2) "341.9374" 
 3) 1) "106.49999767541885376" 
 2) "29.52999957900659211" 
 2) 1) "xian" 
 2) "483.8340" 
 3) 1) "108.96000176668167114" 
 2) "34.25999964418929977"

GEORADIUSBYMEMBER

 #找出位于指定元素周围的其他元素! 
 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 chongqi 
1) "wx4fbxxfke0" 
2) "wm5xzrybty0"
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

缺点:需要有一定的容错
如果不允许容错,就使用 set 或者自己的数据类型即可!

Bitmap 位图

作用:位存储 比如统计用户信息,活跃,不活跃! 登录 、 未登录! 打卡,365打卡!

Bitmap 位图,数据结构! 都是操作二进制位来进行记录,就只有0 和 1 两个状态!
365 天 = 365 bit 1字节 = 8bit 46 个字节左右!

使用bitmap 来记录 周一到周日的打卡!
请添加图片描述

查看某一天是否有打卡!

 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 事务本质一组命令的集合! 一个事务中的所有命令都会被序列化,在事务执行过程的中,会按
照顺序执行!

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

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

Redis事务没有没有隔离级别的概念
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec
Redis单条命令式保存原子性的,但是事务不保证原子性

redis的事务

  • 开启事务(multi)
  • 命令入队(…)
  • 执行事务(exec)

执行事务

 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> 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 # 事务队列中命令都不会被执行!
(nil)

编译型异常(代码有问题! 命令有错!) ,事务中所有的命令都不会被执行!

请添加图片描述

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

请添加图片描述

Watch 监控

悲观锁:很悲观,认为什么时候都会出问题,无论做什么都会加锁!
乐观锁:很乐观,认为什么时候都不会出问题,所以不会上锁! 更新数据的时候去判断一下,在此期间是否有人修改过这个数据,
获取version 更新的时候比较 version

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

执行失败

 127.0.0.1:6379> watch money # 监视 money 
 OK
 127.0.0.1:6379> multi 
 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 # 执行之前,另外一个线程,修改了我们的值,这个时候,就会导致事务执行失 败!
 (nil)

如果修改失败,获取最新的值就好
请添加图片描述

四、Jedis与springboot整合

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

1、导入对应的依赖

<!--导入jedis的包--> <dependencies> <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
 <dependency>
 <groupId>redis.clients</groupId> 
 <artifactId>jedis</artifactId>
 <version>3.2.0</version> 
 </dependency> 
 <!--fastjson--> 
 <dependency>
  <groupId>com.alibaba</groupId> 
  <artifactId>fastjson</artifactId> 
  <version>1.2.62</version> </dependency>
 </dependencies>

2、编码测试:

  • 连接数据库
  • 操作命令
  • 断开连接!

请添加图片描述

常用的API

String
List
Set
Hash
Zset
所有的api命令,对应的上面的redis指令,没有变化!

SpringBoot 整合

SpringBoot 操作数据:spring-data jpa jdbc mongodb redis!
SpringData 也是和 SpringBoot 齐名的项目!
说明: 在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce?
jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接
池! 更像 BIO 模式
lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据
了,更像 NIO 模式
源码

@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; 
 } 
 }
1、导入依赖
<!-- 操作redis --> 
<dependency> 
<groupId>org.springframework.boot</groupId> 
<artifactId>spring-boot-starter-data-redis</artifactId> 
</dependency>
2、配置连接
# 配置
redis spring.redis.host=127.0.0.1 
spring.redis.port=6379
3、测试
@SpringBootTest 
class Redis02SpringbootApplicationTests { 
@Autowired private RedisTemplate redisTemplate; 
@Test 
void contextLoads() {
 // redisTemplate 操作不同的数据类型,api和我们的指令是一样的 
 // opsForValue 操作字符串 类似String 
 // opsForList 操作List 类似List 
 // opsForSet 
 // opsForHash 
 // opsForZSet 
 // opsForGeo 
 // opsForHyperLogLog 
 // 除了进本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的 CRUD 
 // 获取redis的连接对象 
 // RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); 
 // connection.flushDb(); 
 // connection.flushAll(); 
 redisTemplate.opsForValue().set("mykey","reidsVla"); System.out.println(redisTemplate.opsForValue().get("mykey")); }
 }

五、持久化

Redis容器配置redis.conf
redis容器里边的配置文件是需要在创建容器时映射进来的

停止容器:docker container stop myredis
删除容器:docker container rm myredis

重新开始创建容器

1. 创建docker统一的外部配置文件

mkdir -p docker/redis/{conf,data}

2. 在conf目录创建redis.conf的配置文件

touch /docker/redis/conf/redis.conf

3. redis.conf文件的内容需要自行去下载,网上很多

4. 创建启动容器,加载配置文件并持久化数据

docker run -d --privileged=true -p 6379:6379 -v /docker/redis/conf/redis.conf:/etc/redis/redis.conf -v /docker/redis/data:/data --name myredis redis redis-server /etc/redis/redis.conf --appendonly yes

文件目录

/docker/redis
1、简介

什么是持久化?
利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化。

为什么要持久化
防止数据的意外丢失,确保数据安全性

持久化过程保存什么

  • 将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据
  • 将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂,关注点在数据的操作过程
    在这里插入图片描述
2、RDB

RDB启动方式——save

命令 save

作用 手动执行一次保存操作

RDB启动方式—save

指令工作原理
在这里插入图片描述
注意:save指令的执行会阻塞当前Redis服务器,直到当前RDB过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用。

RDB启动方式—bgsave

在这里插入图片描述
注意: bgsave命令是针对save阻塞问题做的优化。Redis内部所有涉及到RDB操作都采用bgsave的方式,save命令可以放弃使用,推荐使用bgsave
bgsave的保存操作可以通过redis的日志查看

docker logs myredis
RDB启动方式 —save配置

配置

save second changes

作用
满足限定时间范围内key的变化数量达到指定数量即进行持久化

参数
second:监控时间范围
changes:监控key的变化量
在conf文件中进行配置

RDB启动方式 —save配置原理

在这里插入图片描述
注意:
save配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的
save配置中对于second与changes设置通常具有互补对应关系(一个大一个小),尽量不要设置成包含性关系
save配置启动后执行的是bgsave操作

RDB启动方式对比
在这里插入图片描述

RDB优缺点

优点

  • RDB是一个紧凑压缩的二进制文件,存储效率较高
  • RDB内部存储的是redis在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景
  • RDB恢复数据的速度要比AOF快很多

应用:服务器中每X小时执行bgsave备份,并将RDB文件拷贝到远程机器中,用于灾难恢复

缺点

  • RDB方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据
  • bgsave指令每次运行要执行fork操作创建子进程,要牺牲掉一些性能
  • Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象
3.AOF
AOF概念

AOF(append onlyfile)持久化:

  • 以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令,以达到恢复数据的目的。
  • 与RDB相比可以简单描述为改记录数据为记录数据产生的过程AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式
AOF写数据过程

在这里插入图片描述

AOF写数据三种策略(appendfsync)

always
每次写入操作均同步到AOF文件中,数据零误差,性能较低,不建议使用
everysec
每秒将缓冲区中的指令同步到AOF文件中,数据准确性较高,性能较高 ,建议使用,也是默认配置
在系统突然宕机的情况下丢失1秒内的数据
no
由操作系统控制每次同步到AOF文件的周期,整体过程不可控

AOF功能开启

配置

appendonly yes|no

​ 作用 是否开启AOF持久化功能,默认为不开启状态

配置

appendfsync always|everysec|no

作用 AOF写数据策略

AOF重写

作用
降低磁盘占用量,提高磁盘利用率
提高持久化效率,降低持久化写时间,提高IO性能
降低数据恢复用时,提高数据恢复效率

如何使用

手动重写

bgrewriteaof

自动重写

auto-aof-rewrite-min-size size 
auto-aof-rewrite-percentage percentage
工作原理

在这里插入图片描述
AOF自动重写
自动重写触发条件设置

//触发重写的最小大小
auto-aof-rewrite-min-size size 
//触发重写须达到的最小百分比
auto-aof-rewrite-percentage percent

自动重写触发比对参数( 运行指令info Persistence获取具体信息 )

//当前.aof的文件大小
aof_current_size 
//基础文件大小
aof_base_size

自动重写触发条件
在这里插入图片描述
缓冲策略
AOF缓冲区同步文件策略,由参数appendfsync控制

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

RDB VS AOF

在这里插入图片描述
RDB与AOF的选择之惑
对数据非常敏感,建议使用默认的AOF持久化方案
AOF持久化策略使用everysecond,每秒钟fsync一次。该策略redis仍可以保持很好的处理性能,当出现问题时,最多丢失0-1秒内的数据。
注意:由于AOF文件存储体积较大,且恢复速度较慢
数据呈现阶段有效性,建议使用RDB持久化方案
数据可以良好的做到阶段内无丢失(该阶段是开发者或运维人员手工维护的),且恢复速度较快,阶段 点数据恢复通常采用RDB方案
注意:利用RDB实现紧凑的数据持久化会使Redis降的很低
综合比对
RDB与AOF的选择实际上是在做一种权衡,每种都有利有弊
如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用AOF
如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用RDB
灾难恢复选用RDB
双保险策略,同时开启 RDB 和 AOF,重启后,Redis优先使用 AOF 来恢复数据,降低丢失数据

六、删除策略

时效性数据的存储结构
Redis中的数据,在expire中以哈希的方式保存在其中。其value是数据在内存中的地址,filed是对应的生命周期

在这里插入图片描述
数据删除策略的目标
在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体redis性能的下降,甚至引发服务器宕机或内存泄露

数据删除策略

redis使用:定期删除+惰性删除
定期删除:redis默认每个100ms随机抽取key进行过期检查。
惰性删除:获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除

内存淘汰机制
操作:在redis.conf中有一行配置 # maxmemory-policy allkeys-lru
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。不推荐。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐。
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐。
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

  • 定时删除
  • 惰性删除
  • 定期删除

七、redis 主从 哨兵 集群

八、redis问题解决方案

九、redis分布式锁 及面试题

redis分布式锁实现
/**
 * 第一种分布式锁
 */
public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        if (jedis.get(lockKey).equals(requestId)) {
            System.out.println("释放锁..." + Thread.currentThread().getName() + ",identifierValue:" + requestId);
            jedis.del(lockKey);
            return true;
        }
        return false;
    }
}

可重入分布式锁

import java.util.Collections;
import java.util.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

/**
 * Redis可重入锁
 */
public class RedisLock {

    private static final StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
    private static final DefaultRedisScript<Long> LOCK_SCRIPT;
    private static final DefaultRedisScript<Object> UNLOCK_SCRIPT;
    static {
        // 加载释放锁的脚本
        LOCK_SCRIPT = new DefaultRedisScript<>();
        LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
        LOCK_SCRIPT.setResultType(Long.class);

        // 加载释放锁的脚本
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
    }
    /**
     * 获取锁
     * @param lockName 锁名称
     * @param releaseTime 超时时间(单位:秒)
     * @return key 解锁标识
     */
    public static String tryLock(String lockName,String releaseTime) {
        // 存入的线程信息的前缀,防止与其它JVM中线程信息冲突
        String key = UUID.randomUUID().toString();

        // 执行脚本
        Long result = redisTemplate.execute(
                LOCK_SCRIPT,
                Collections.singletonList(lockName),
                key + Thread.currentThread().getId(), releaseTime);

        // 判断结果
        if(result != null && result.intValue() == 1) {
            return key;
        }else {
            return null;
        }
    }
    /**
     * 释放锁
     * @param lockName 锁名称
     * @param key 解锁标识
     */
    public static void unlock(String lockName,String key) {
        // 执行脚本
        redisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(lockName),
                key + Thread.currentThread().getId(), null);
    }
}
问题一 redis为什么是高效的?

1、完全基于内存,大多数请求都是内存操作,非常快速;
2、数据结构简单,操作简单;
3、采用单线程,避免了不必要的上下文切换和竞争条件,不存在多进程或者多线程的切换,不用考虑锁带来的性能消耗;
4、使用多路 I/O复用模型,非阻塞 IO

可以看看这个
多路 I/O复用模型

问题二 如何解决缓存一致性问题?

对于缓存和数据库的操作,主要有以下两种方式。
1.先删缓存,再更新数据库

先删除缓存,数据库还没有更新成功,此时如果读取缓存,缓存不存在,去数据库中读取到的是旧值,缓存不一致发生。

延时双删

延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再 Sleep 一段时间,然后再次删除缓存。

Sleep 的时间要对业务读写缓存的时间做出评估,Sleep 时间大于读写缓存的时间即可。

流程如下:

  • 线程1删除缓存,然后去更新数据库;
  • 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存;
  • 线程1,根据估算的时间,Sleep,由于Sleep 的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除;
  • 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值。

2.先更新数据库,再删除缓存
如果反过来操作,先更新数据库,再删除缓存呢?

这个就更明显的问题了,更新数据库成功,如果删除缓存失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致。

解决方案
消息队列
这是网上很多文章里都有写过的方案。但是这个方案的缺陷会更明显一点。
先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。

进阶版消息队列
为了解决缓存一致性的问题单独引入一个消息队列,太复杂了。
其实,一般大公司本身都会有监听 binlog 消息的消息队列存在,主要是为了做一些核对的工作。
这样,我们可以借助监听 binlog 的消息队列来做删除缓存的操作。这样做的好处是,不用你自己引入,侵入到你的业务代码中,中间件帮你做了解耦,同时,中间件的这个东西本身就保证了高可用。
当然,这样消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。
而且,如果并发不是特别高的话,这种做法的实时性和一致性都还算可以接受的。

其他解决方案
设置缓存过期时间
每次放入缓存的时候,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。
如果对于一致性要求不是很高的情况,可以采用这种方案。
这个方案还会有另外一个问题,就是如果数据更新的特别频繁,不一致性的问题就很大了。
在实际生产中,我们有一些活动的缓存数据是使用这种方式处理的。
因为活动并不频繁发生改变,而且对于活动来说,短暂的不一致性并不会有什么大的问题。

为什么是删除,而不是更新缓存?
我们以先更新数据库,再删除缓存来举例。
如果是更新的话,那就是先更新数据库,再更新缓存。
举个例子:如果数据库 1 小时内更新了 1000 次,那么缓存也要更新 1000 次,但是这个缓存可能在1小时内只被读取了 1 次,那么这 1000 次的更新有必要吗?
反过来,如果是删除的话,就算数据库更新了 1000 次,那么也只是做了 1 次缓存删除,只有当缓存真正被读取的时候才去数据库加载。

总结
首先,我们要明确一点,缓存不是更新,而应该是删除。
删除缓存有两种方式:

  • 先删除缓存,再更新数据库。解决方案是使用延迟双删
  • 先更新数据库,再删除缓存。解决方案是消息队列或者其他 binlog同步,引入消息队列会带来更多的问题,并不推荐直接使用。
  • 针对缓存一致性要求不是很高的场景,那么只通过设置超时时间就可以了。

其实,如果不是很高的并发,无论你选择先删缓存还是后删缓存的方式,都几乎很少能产生这种问题,但是在高并发下,你应该知道怎么解决问题。

问题答案来源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值