关于Redis的基础知识,我就不多说了。我再其他博客里面介绍了一些了。本博文主要汇总,思考总结一些Redis的一些使用场景,解决什么类型的问题。
主要内容有:
1.Redis基础:
- 数据类型
- 通用命令
- Jedis
2.高级
- 持久化
- redis.conf
- 事务
- 集群
3.应用
五种数据结构
可以做个类比:
1.关于数据类型,及其应用场景
String类型的数据结构
1.基本操作:
set
get
delete 成功返回1
2.在set get之前加个m (multiple多个)
mset
mget
一次设置多个,一次取多个。
获取数据字符个数:
往字符串后面追加:原始数据存在就追加 不存在就新创建
但数据操作与多数据操作的选择问题:
set key value
mset key1 value1 key2 value2 .....
注意:
客户单发送指令消耗时间 到达之后执行消耗时间 结果返回消耗时间
但是: 指令多,返回的数据也会多,需要综合考量。发送时间长和执行时间,数据发送量等等权衡。
String类型数据的扩展操作:
场景1
分表,防止主键重复问题,MySQL不能实现,通过redis实现。
使用:
加:incr key
减:decr key:
一次增减一个自然单位长度(字符串是个纯数字)。
加:给定指定整数增加值:
给定指定小数增加值:
增加: decr 减小 decrby
小结:
加法:
incry key 自然长度
incrby key 10 指定长度
incrbyfloat 小数
减法:
decr key
decrby key increment
注意:
string 作为数值操作:
- string在redis内部存储默认是一个字符串,当遇到增加减少操作incr,decr时会抓成数值型进行计算。
- redis所有的操作都是原子性的,采用单线程处理所有的业务,命令式一个一个执行的,因此无需考虑并发带来的影响。
- 如果查出了redis上限范围,将报错。long.MAX_VALUE
总结应用场景: 主键生成策略,保证唯一性。
场景2
超级女声投票,1周之内。
设置数据具有指定声明周期
setex tel 10 1 #值是10 时间是1s 注意:毫秒
setex tel 10 1 #值是10 时间是1ms 注意:秒
String类型数据操作的注意事项:
- 数据操作不成功的反馈与数据正常操作之间的差异 : 成功1 失败0
- 表示运行结果值: 3 3个
数据查询不到时候: nil 等同于 null
string最大存储量: 512M
纯数字时候最大范围是 Long的最大值
string应用场景,主页高频信息控制。新浪大V粉丝数
eg: user:id:234:fans -> 234234 表名主键值属性名作为key
比如当心粉丝来了时候 直接使用redis的自增处理之
或者value 存放个Json数据,信息量比较大。
key的设置规范习惯:
hash类型;
string 可以用来存储 JSON,这样就有个困惑。修改数据比较麻烦。
所以可以这么设计:
进而改进为: 属性对应的值: key value的感觉
- 新的存储需求: 对一系列存储的数据进行编组,方便管理,典型应用存储对象信息。(典型的存对象)
- 需要的存储结构:一个存储空间报错多个键值对数据
- hash类型: 底层使用哈希表结构实现数据存储
注意: hash存储结构优化
- 如果field数量较少,存储结构优化为类数组结构
- 如果field数量较多,存储结构使用HashMap结构
基本操作(单个):(光有key不行,还得有field)
- 添加、修改数据: hset key field value
- 获取数 hget key field 、hgetall key
- 删除数据 hdel key
基本操作(多个)
- 修改多个数据: hmset field1 value1 field2 value2
- 获取多个数据 hmget key field1 field2
- 获取哈希表中字段数量: hlen key
- 获取哈希表中是否存在指定字段 hexists key field
符合redis的脾气,有就改,没有就创建:
扩展用法:
- 获取哈希表中所有的字段名或字段值: hkeys key hvals key
- 设置指定字段的数值数据增加指定范围的值
还可以自增:
注意事项:
- hash类型下的value只能存储字符串,不允许存储其他数据类型,不存在嵌套现象。如果数据获取不到,对应nil
- 每个hash可以存储2^32 - 1个键值对
- hash类型十分贴近对象的数据存储形式,并且可以灵活添加删除对象属性。但hash设计初衷不是为了存储大量对象而设计的,切记不可滥用,更不要将hash作为对象列表使用。
- hgetall操作可以获取全部属性,如果内部field过多,遍历整体数据效率就会很低,有可能成为数据访问瓶颈。
·hash业务场景1:购物车
一个购物车里面,好多商品,对应一个Id。
解决方案:
- 以客户id为key,每位客户创建一个hash存储结构存储对应的购物车信息
- 将商品编号作为field,购买数量作为value进行存储
- 添加商品:追加全新的filed与value
- 浏览:遍历hash
- 更改数量: 自增/自检,设置value值
- 删除商品: 删除field
- 清空: 删除key
001用户的 购物车 g01 g02 g03
没有加速购物车呈现,只有商品ID和数量。
- 每条购物车中的商品记录保存成两条field
- field1 专用于保存购买数量:
命名格式: 商品id:nums
保存数量: 数值
- field2专用于保存购物车中显示的信息,包含文字描述,图片地址,所属商家信息等
命名格式: 商品id:info
保存数据:json
这样数量和信息(json)都有了 图片、地址、描述信息提取出来(但是商品信息也是会重复的!独立起起来)
独立的hash: 专门用来保存商品的hash
使用: hsetnx key field value
没有就操作,有就不操作了:
·hash业务场景2:
抢购
不同的商品数量存储和销售数量减少1、20
尽量不要把redis放在redis,比如超卖,是否有值等等。
注意:redis应用于抢购,限购类,限量发放优惠券,激活码等业务的数据存储设计。
区分下用string存对象和hash存对象的问题:
- string存储对象(json)与hash存储对象。 一个是全局,一个是局部。根据情况灵活设计
list:
list类型
- 数据存储需求: 存储多个数据,并对数据进入存储空间的顺序进行区分
- 需要的存储结构:一个存储,空间保存多个数据,并且通过数据可以体现进入顺序
- list类型:保存多个数据,底层使用双向链表存储结构实现
操作:
- 添加/修改数据 lpush key value1 rpush key value1
- 获取数据 lrange key start stop( 注意start 和 stop为索引,可以为负数 比如 stop为-1 则为倒数第二个) lindex key index llen key
- 获取并移除数据 lpop key rpop key
右进,左查
反着查(不知道添加了多少个,还是想看全部的情况)
看倒数第二:
看每个索引上的数据:
重点看下,怎么进怎么出:
list类型数据扩展操作:
- 规定时间内获取并移除数据 blpop key1 timeout brpop key1 timeout 注释: b代表阻塞的意思
存值然后取值的常规操作:
可以打开两个客户端:
客户端a
客户端b
在b没有放入值时候,a一直等待,等待不了就nil,有结果就直接返回。
也可以这么使用: a等待一堆list ,哪个有值了就取出来哪个。即为: blpop list1 list2 list3 timeout
业务场景:
微信朋友圈点赞,要求按照点赞顺序显示点赞好友信息,取消点赞,删除对应好友信息。 lrem key count value 解释: 从左边删除哪里的 第几个(因为有序的哈) 删除什么
有顺序,又多个,自然redis的list数据结构了
删除的操作: 注意方向 数量 内容
总结: redis应用具有操作先后顺序的数据控制
list类型数据操作足以事项:
- list中保存的数据都是string类型的,数据总容量是有限的,最多2^32-1个元素
- list具有索引概念,但是操作数据时通常以队列的形式进行入队出队操作,或以栈的形式进行入栈出栈操作
- 获取全部数据操作结束索引设置为-1
- list可以对数据进行分页操作,通常第一页信息来自于list,第二页及更多的信息通过数据库的形式加载
业务场景:
新浪微博中个人用户的关注猎豹需要按照用户的关注顺序进行展示,粉丝列表需要将最近关注的粉丝列放在最前面。
新闻,资讯放到最前面,按照时间发生展示?
企业运营过程中,系统产生的大量运营数据,如何包装多态服务器操作日志的统一顺序输出?
解决方案
- 依赖lis的数据具有顺序的特征对信息进行管理
- 使用队列模型解决多路信息汇总合并的问题
- 使用栈模型解决最新消息的问题
日志消息聚集,顺序:
统一查询: lrange logs 0 -1
set类型
- 新的存储需求: 存储大量的数,在查询方面提供更高的效率。
- 需要的存储结构:能够保存大量的数据,高效的内部存储机制,便于查询。
hash数据类型,我们用的是value,如果我们用key呢? 何尝不可?
于是,变形,右边全部放空:
于是:
set类型数据的基本操作:
- 添加数据 : sadd key member1 [member2]
- 获取全部数据: smembers key
- 删除数据: srem key member1 [member2]
set部分左右,hash结构,没有索引,操作就比较简单了。
- 获取集合数据总量: scard key
- 判断集合中是否包含指定数据: sismember key member
应用场景:
每位用户首次使用今日头条是会设置3项爱好的内容,但是后期为了增加用户的活跃度,兴趣点,必须让用户对其他信息类别逐渐产生兴趣,增加客户留存度,如何实现?
解决方案:
随机获取集合中指定数量的数据
srandmember key count (原集合数据不变)
随机获取集合中的某个数据并将该数据移除集合
spop key (原集合数据减少)
随机获取:
随机剔除:
比如任务分配: 随机挑选一个去处理。
总结: redis应用于随机推荐类信息检索,例如热点歌单推荐,热点新闻推荐等等
应用场景:
社交软件为了存进用户间的交流(共同关注),微博增加用户热度(有多少个朋友关注),qq推荐好友,
set类型数据的扩展操作:
解决方案:
- 求两个集合的交,并,差集:
sinter key1 [key2]
sunion key1 [key2]
sdiff key1 [key2]
- 求两个集合的并,交、差集并存储到指定集合中
sinterstore destination key1 [key2]
sunterstore destination key1 [key2]
sdiffstore destination key1 [key2]
- 将指定数据从原始集合汇总移动到目标集合中
smove source destination member
移动w1 从u2移动到u1
总结:
- redis应用于同类信息的关联搜索,二度关联搜索,深度关联搜索
- 显示共同关注(一度)
- 显示共同好友 (一度)
- 由用户A出发,获取到好友用户B的好友信息列表(一度)
- 由用户A出发,获取到好友用户B的购物清单列表(二度)
- 由用户A出发,获取到好友用户B的游戏充值列表(二度)
注意: set类型数据操作的注意事项
- set类型不允许数据重复,如果添加的数据在set中已经存在,将只保留一份。
- set虽然与hash的存储结构相同,但是无法启用hash中存储值的空间
场景:
120000员工,内部OA系统中具有800个角色,3000个业务操作,23400多种数据,每个员工具有一个或者多个角色,如何快速进行业务操作的权限校验?
解决方案:
- 依赖set集合数据不重复的特征,依赖set集合hash存储结构特征完成数据过滤与快速查询
- 依据用户id获取用户所有角色
- 依据用户所有角色获取用户所有操作权限放入set集合
- 依据用户所有角色获取用户所有数据全选放入set集合
001 和 002用户分配权限:
合并:
校验方式1:代码看是不是在这个集合里面(推荐这个,业务和数据分开)
校验方式2:sismember指令
注意 redis提供基础数还是提供校验结果,推荐校验结果。
set应用场景:
公司对旗下新的网站做推广,统计网站pv(访问量),UV(独立访客),IP(独立IP)
注:
pv:网站备份昂文次数,可以通过刷新网页提高访问量
uv: 网站被不同用户访问的次数,可以通过cookie统计访问量,相同用户切换ip地址,uv不变
ip: 网站被不用IP地址访问的总次数,可以通过ip地址统计访问量,相同ip不同用户访问,ip地址不变
解决方案:
- 利用set结婚的数据去重特性,记录各种访问数据
- 建立string类型数据,利用incr统计日访问量(pv)
- 建立set模型,记录不同cookie数量(uv)
- 建立set模型,记录不同IP数量(IP)
重复的进不来了: 快速去重
业务场景:
黑名单
爬虫技术,快速获取信息。克隆别人的数据到自己库里。爬虫的访问量不算的。
解决方案:
- 基于经营战略设定问题用户发现,鉴别规则
- 周期性更新满足规则的用户黑名单,加入set集合
- 用户行为信息达到后与黑名单进行比对,确认行为去向
- 黑名单过滤ip地址: 应用于开放游客访问权限的信息源
- 黑名单过滤设备信息: 应用于限定访问设备的信息源
- 黑名单过滤用户: 应用于基于访问权限的信息源
总结: 基于redis 可以做黑白名单的控制
sorted_set 类型
- 新的存储需求: 数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式。
- 需要的存储结构: 新的存储模型,可以保存可排序的数据
- sorted_set类型: 在set的存储结构基础上添加可排序字段
对应的基本操作:
- 添加数据
- zadd key score1 member1 [score2 member2]
- 获取全部数据
- zrange key start stop
- zrevrange key start stop
- 删除数据
- zrem key member
存值和获取(已经排序了的)
获取详情的: 数据和排序字段值
反向:由小到大
删除操作:
- 按条件获取数据
- zrangebyscore key min max
- zrevrangebyscore key max min
- 条件删除数据
- zremrangebyrank key start stop
- zremrangebyscore key min max
类似于limit的操作:
删除:60 ~ 70
通过索引删除:
删除索引 0 ~1之间的
注意:
- min与max用于限定查询的搜索条件
- start与stop用于限定查询范围,作用于索引,表示开始和结束索引
- offset与count用于限定查询范围,作用于查询结果,表示开始位置和数据总量
- 获取集合数据总量
- zcard key
- zcount key min max
- 结合交、并操作
- zinterstore destination numkeys key [key ...]
- zunionstore destination numkeys key [key ...]
操作:
127.0.0.1:6379> zcard scores
(integer) 0
127.0.0.1:6379> zcount scores 99 200
(integer) 0
127.0.0.1:6379>
存储个集合数据:
127.0.0.1:6379> zcard scores
(integer) 0
127.0.0.1:6379> zcount scores 99 200
(integer) 0
127.0.0.1:6379>
操作:三个注意和后面的集合数量对应
合并后:有相同属性的值都累加到一起了。除了交还有求和
合并完毕之后,可以进行求最大最小值等操作
127.0.0.1:6379> zinterstore sss 3 s1 s2 s3 aggregate max
(integer) 2
业务场景: 榜单类的。除了数量,还要有排序。 sorted_set刚好有排序功能
票选“快女”前十。
各类资源网站top10
聊天室活跃度
游戏好友亲密度
解决方案:
- 获取数据对应的索引(排名)
- zrank key member
- zrevrank key member
- score值获取与修改
- zscore key member
- zincrby key increment menber
存放电影排行榜,并且获取排名(索引)
127.0.0.1:6379> zinterstore sss 3 s1 s2 s3 aggregate max
(integer) 2
127.0.0.1:6379> zadd moives 143 aa 97 bb 201 cc
(integer) 3
127.0.0.1:6379> zrank moives bb
(integer) 0
127.0.0.1:6379> zrevrank moives bb
(integer) 2
获取值:
127.0.0.1:6379> zscore moives aa
"143"
又有3人来投票了:
127.0.0.1:6379> zincrby movies 1 aa
"4"
127.0.0.1:6379> zscore movies aa
"4"
各种投票结束后,可以合并下看看~
注意:scored_set
- score保存的数据存储空间是64位
- score保存的数据也可以是一个双精度的double值,基于双精度浮点的特征,可能会丢失精度,使用时候要谨慎
- sorted_set底层存储还是基于set结构的,因此数据不能重复,如果重复添加相同的数据,score值将被反复覆盖,保留最后一次修改的结果。
业务场景:
有期会员,网盘使用加速
解决方案:
- 对于基于时间线氙灯的任务处理,将处理时间记录为score值,利用排序功能邠处理的先后顺序。
- 记录下一个要处理的时间,当到期后处理对应任务,移除redis总的记录,并记录下一个要处理的时间。
- 当新任务加入时候,判定并更新当前下一个要处理的任务时间。
- 为提升sorted_set的性能,通常将任务根据特征存储成若干个sorted_sorted. 例如:1小时内,1天内,周内,月内,季内,年度等,将即将操作的若干个任务纳入到1小时内处理的队列中。
腾讯视频的会员模拟:
127.0.0.1:6379> zadd ts 1609785 uid:001
(integer) 1
127.0.0.1:6379> zadd ts 1609235 uid:002
(integer) 1
127.0.0.1:6379> zadd ts 1609135 uid:003
(integer) 1
查看:
127.0.0.1:6379> zrange ts 0 -1 withscores
1) "uid:003"
2) "1609135"
3) "uid:002"
4) "1609235"
5) "uid:001"
6) "1609785"
到期期间一目了然,到期就移出去。
记录的是下一个被提醒的用户,对应的提醒时间。
redis中获取当前系统时间:
127.0.0.1:6379> time
1) "1575736433"
2) "275262"
总结: redis应用于定时任务顺序管理或任务过期管理
应用场景:
任务/消息权重设定应用
当任务或者消息等待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,如何实现任务权重管理。
127.0.0.1:6379> zadd tasks 4 order:id:005
(integer) 1
127.0.0.1:6379> zadd tasks 1 order:id:023
(integer) 1
127.0.0.1:6379> zadd tasks 9 order:id:012
(integer) 1
127.0.0.1:6379> zrevrange tasks 0 -1 withscores
1) "order:id:012"
2) "9"
3) "order:id:005"
4) "4"
5) "order:id:023"
6) "1"
移除掉:
127.0.0.1:6379> zrevrange tasks 0 0
1) "order:id:012"
127.0.0.1:6379> zrem tasks order:id:023
(integer) 1
127.0.0.1:6379> zrevrange tasks 0 -1 withscores
1) "order:id:012"
2) "9"
3) "order:id:005"
4) "4"
- 对于带有权重的任务,优先处理权重高的任务,采用score记录权重即可多条件任务权重设定
- 如果权重条件过多时候,需要对排序score值进行处理,保障score值能够兼容2条件或者多条件。例如外贸订单优先于国内订单,总裁订单优先于员工订单,经理订单优先于员工订单。
- 因score长度受限,需要对数据进行阶段处理,尤其是时间设置为小时或者分钟级即可(折算后)
- 先设定订单级别,后设定订单发起角色类别,整体score长度必须是统一的,不足位补0.第一排序规则首位不得是0。
127.0.0.1:6379> zadd tt 102004 order:id:1
(integer) 1
127.0.0.1:6379> zadd tt 102008 order:id:2
(integer) 1
127.0.0.1:6379> zrevrange tt 0 -1 withscores
1) "order:id:2"
2) "102008"
3) "order:id:1"
4) "102004"
正序看就行:
127.0.0.1:6379> zrange tt 0 -1 withscores
1) "order:id:1"
2) "102004"
3) "order:id:2"
4) "102008"
综合经验总结:
限定每个用户每分钟最多发起10次调用。
解决方案:
- 设计计数器,记录调用次数,用于,通知业务执行次数。以用户id作为key,使用次数作为value
- 在调用前获取次数,判断是否超过限定次数。不超过,每次调用+1, 业务调用失败,计数-1
- 为计数器设置生命周期指定周期,例如1秒/分钟,自动清除周期内使用次数。
指令模拟:
127.0.0.1:6379> get 415
(nil)
127.0.0.1:6379> setex 415 60 1
OK
127.0.0.1:6379> get 415
"1"
127.0.0.1:6379> incr 415
(integer) 2
127.0.0.1:6379> incr 415
(integer) 3
127.0.0.1:6379>
解决方案改良:
- 取消最大值的判定,利用incr操作超过最大值抛出异常的形式替代每次判断是否大于最大值
- 判断是否为nil:是,设置max次数。 不是,计数+1, 业务调用失败,计数-1
- 遇到异常即+操作超过上限,视为使用达到上限。用上限异常来搞定判断。
总结: rdis应用于限时按次结算的服务控制
应用场景:
微信接受消息后,会默认将最近接受的消息置顶,当多个好友及关注的订阅号同时发送消息时,该排序会不停的进行交替。
解决方案:
- 依赖list的数据就有顺序的特征对消息进行管理,将list结构作为栈使用。
- 对置顶与普通会话分别创建独立的list分别管理
- 当某个list中接收到用户消息后,将消息发送放的id从list的一侧加入list(设定为左侧)
- 多个相同id发出的消息反复入栈会出现问题,在入栈之前无论是否具有当前id对应的消息,先删除对应id
- 推送消息时先推送指定会话list,再推送普通会话list,推送完成的list清除所有数据
- 消息的数量,也就是微信用户对话数量采用计数器的思想另行记录,伴随list操作同步更新。
模拟下:
127.0.0.1:6379> lrem 100 1 200
(integer) 0
127.0.0.1:6379> lpush 100 200
(integer) 1
127.0.0.1:6379> lrem 100 1 300
(integer) 0
127.0.0.1:6379> lpush 100 300
(integer) 2
127.0.0.1:6379> lrem 100 1 400
(integer) 0
127.0.0.1:6379> lpush 100 400
(integer) 3
127.0.0.1:6379> lrem 100 1 200
(integer) 1
127.0.0.1:6379> lpush 100 200
(integer) 3
127.0.0.1:6379> lrem 100 1 300
(integer) 1
127.0.0.1:6379> lpush 100 300
(integer) 3
127.0.0.1:6379> lrange 100 0 -1
1) "300"
2) "200"
3) "400"
总计: redis应用于基于时间顺序的数据操作,而不关注具体时间。
key特征:
key是一个字符串,通过key获取redis中保存的数据。
key应该设计的操作:
1.对key自身状态的操作: 例如: 删除,判定存在,获取类型等
127.0.0.1:6379> set str str
OK
127.0.0.1:6379> hset hash1 hash1 hash1
(integer) 0
127.0.0.1:6379> lpush list1 list1
(integer) 5
127.0.0.1:6379> sadd set1 set1
(integer) 0
127.0.0.1:6379> zadd zset1 1 zset1
(integer) 0
127.0.0.1:6379> type zset1
zset
127.0.0.1:6379> type str
string
127.0.0.1:6379> type hash1
hash
127.0.0.1:6379> exists str
(integer) 1
127.0.0.1:6379> del zset1
(integer) 1
127.0.0.1:6379> del zset1
(integer) 0
127.0.0.1:6379> exists zset1
(integer) 0
2.对key有效性控制的操作: 例如: 有效期设定,判定是否有效,有效状态的切换
- 为指定key设置有效期
- pexpire key milliseconds
- expireat key timestamp
- pexpireat key milliseconds-timesamp
- expire key seconds
获取key的有效时间
-
- ttl key
- pptl key
切换key从时效性转换为永久性
-
- persist key
127.0.0.1:6379> set str str
OK
127.0.0.1:6379> lpush list1 list1
(integer) 6
127.0.0.1:6379> lpush list1 list2
(integer) 7
127.0.0.1:6379> expire str 3
(integer) 1
127.0.0.1:6379> get str
"str"
127.0.0.1:6379> get str
(nil)
查看剩余时间: -2 代表key不存在。 key存在但是没有剩余时间则返回-1
127.0.0.1:6379> ttl list1
(integer) 25
127.0.0.1:6379> ttl list1
(integer) -2
127.0.0.1:6379> expire list2 60
(integer) 1
127.0.0.1:6379> persist list2
(integer) 1
127.0.0.1:6379> ttl list2
(integer) -1
3.对key快速查询操作,例如:按指定策略查询key
查询key
key pattern * 匹配任意数量的任意字符 ?配合一个任意符号 [] 匹配一个指定符号
127.0.0.1:6379> keys *
1) "p01"
2) "ss"
3) "b"
4) "name"
5) "s3"
6) "tt"
为key改名
rename key newkey
renamenx key newkey
对所有key排序
sort
其他key通用操作: help @generic
Jedis相关
pom:
redis.clients
jedis
3.0.0
test api:
@Test public void test1(){ Jedis jedis = new Jedis("192.168.91.1", 6379); // jedis.set("name", "toov5"); String name = jedis.get("name"); System.out.println(name); jedis.close(); } //操作list @Test public void test2(){ Jedis jedis = new Jedis("192.168.91.1", 6379); jedis.lpush("list1", "a", "b", "c"); jedis.rpush("list1","x"); Listlist1 = jedis.lrange("list1", 0, -1); for (String s : list1){ System.out.println(s); } //查长度 System.out.println(jedis.llen("list1")); //存储哈希 jedis.hset("hash1", "a1", "a1"); jedis.hset("hash1", "a2", "a2"); jedis.hset("hash1", "a3", "a2"); Map hash1 = jedis.hgetAll("hash1"); System.out.println(hash1); //查看长度 System.out.println(jedis.hlen("hash1")); jedis.close(); }
案例: 每分钟调用次数的限制。
public class Service { //控制单元 public void service(String userName){ Jedis jedis = new Jedis("192.168.91.1", 6379); String user = jedis.get("com" + userName); try { if (null == user){ //不存在 jedis.setex("com" + userName, 20, Long.MAX_VALUE - 10+ ""); }else { // 存在 Long incr = jedis.incr("com" + userName); //调用业务 business(userName,10 - (Long.MAX_VALUE - incr)); } }catch (JedisDataException e){ System.out.println("次数到达限制,请充值"); return; }finally { jedis.close(); } } public void business(String user, Long incr){ System.out.println(user + "业务操作执行"+ incr + "次"); } } class MyThread extends Thread{ Service sc = new Service(); @Override public void run() { while (true){ sc.service("会员一级"); try { Thread.sleep(1000L); }catch (InterruptedException e){ e.printStackTrace(); } } } } class Main{ public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); }
到时了之后,会从0开始计数了又。
public class Service { private String userName; private Integer times; public Service(String name, Integer times){ this.userName = name; this.times = times; } //控制单元 public void service(){ Jedis jedis = new Jedis("192.168.91.1", 6379); String user = jedis.get("com" + userName); try { if (null == user){ //不存在 jedis.setex("com" + userName, 20, Long.MAX_VALUE - times+ ""); }else { // 存在 Long incr = jedis.incr("com" + userName); //调用业务 business(userName,times - (Long.MAX_VALUE - incr)); } }catch (JedisDataException e){ System.out.println(userName + "次数到达限制,请充值"); return; }finally { jedis.close(); } } public void business(String user, Long incr){ System.out.println(user + "业务操作执行"+ incr + "次"); } } class MyThread extends Thread{ Service sc; public MyThread(String name, int times){ sc = new Service(name, times); } @Override public void run() { while (true){ sc.service(); try { Thread.sleep(1000L); }catch (InterruptedException e){ e.printStackTrace(); } } } } class Main{ public static void main(String[] args) { MyThread mt1 = new MyThread("普通用户", 3); mt1.start(); MyThread mt2 = new MyThread("vip用户", 6); mt2.start(); } }
线程模拟多用户。
Jedis工具类:
注JedisPoll: Jedis连接池。
public class JedisUtils { private static JedisPool jedisPool; private static String host; private static int port; private static int maxTotal; private static int maxIdle; static { //加载配置文件 ResourceBundle resourceBundle = ResourceBundle.getBundle("redis"); host = resourceBundle.getString("redis.host"); port = Integer.parseInt(resourceBundle.getString("redis.port")); maxTotal = Integer.parseInt(resourceBundle.getString("redis.maxTotal")); maxIdle = Integer.parseInt(resourceBundle.getString("redis.maxIdle")); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); //最大连接数 jedisPoolConfig.setMaxTotal(30); //活动连接数 jedisPoolConfig.setMaxIdle(10); String host = "192.168.91.1"; int port = 6379; jedisPool = new JedisPool(jedisPoolConfig, host, port); } public static Jedis getJedis(){ return jedisPool.getResource(); } }
关于Redis持久化。
数据持久化的套路: 1.持久化数据(数据快照)。 2.持久化指令(操作的过程,类似于日志)
RDB启动方式 ---save指令
手动执行一次保存操作,会生成rdb文件。保存当前快照信息。
创建一个data目录:
/home/redis/data
修改启动配置文件:
日志生成的目录:
日志文件名字:便于查阅
关于save指令相关配置:
- dbfilename :设置快照的文件名,默认是 dump.rdb 经验: 通常设置为dump-端口号.rdb
- dir:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。默认是和当前配置文件保存在同一目录。 经验:通常设置成存储空间较大的目录中,目录名称为data
- rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
- rdbchecksum :默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
此时断电,数据会保存下来。启动时候会加载。
注意Redis的单线程执行:
Redis的任务序列:
save指令的执行会阻塞当前Redis服务器,知道当前RDB过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用。
总结:数据量过大时候,单线程执行方式造成效率低的问题。
解决方案: 后台执行。
Redis客户端发起指令,redis服务器控制指令执行。合理时间去执行。保存数据。 此时RDB启动方式指令bgsave。租用:说动启动后台保存操作,但是不立即执行。
指令执行原理:
其实是通过系统fork的子进程去实现完成的。
查看下日志:
redis_6379.log
注意: bgsave命令式针对save阻塞问题做的优化。Redis内部所有涉及到RDB擦欧洲哦都是采用bgsave的方式,save命令可以放弃使用。
关于配置:
save 和 bgsave都是RDB形式,所以可以共用。
bgsave: stop-writes-on-bgsave-error yes # 后台存储过程中如果出现错误现象,是否停止保存操作。经验:通常默认开启。
问题: 反复执行保存指令,忘记了,或者不知道数据产生了多少变化,什么时候保存问题。
解决方案:
自动执行配置: save second changes
参数:(单位时间内,达标了就去执行)
second: 监控范围
changes: 监控key的变化量
位置:
在cof文件进行配置
Redis基于某条件,满足时候,保存数据。 满足限定时间范围内key的变化数量达到指定数量即进行持久化。
每10s,2个发生变化就执行保存
关闭Redis: ps aux|grep redis 然后 kill
连续set 两个key值:
查看日志:指令: ll
会发生变化
save配置原理:
注意:
- save配置要根据实际业务情况进行设置,拼读过高或过低都会出现性能问题,结果可能是灾难性的 (上述案例2个key同步一次)
- save配置中对于second与changes设置通常具有互补对应关系,尽量不要设置成包含性关系。 比如前面second小的,后面大
- save配置启动后执行的是bgsave操作
RDB三种启动方式对比:
其他的RDB启动方式,(不太常用)的:
- 全量复制
- 服务器运行过程汇总启动: debug reload
- 关闭服务器时指定保存数据 shutdown save
RDB优点:
- RDB是一个紧凑压缩的二进制文件,存储效率高。
- RDB内部存储的是redis在某个时间点的数据快照,非常适用于数据备份,全量复制等场景。
- RDB回复数的速度要比AOF块很多
- 应用: 服务器中每X小时执行bgsave备份,并将RDB文件拷贝到远程机器中,用于灾难恢复。
RDB缺点:
- RDB方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据。
- bgsave指令每次运行要执行fork操作创建子进程,要消耗掉一些性能。
- Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各种版本服务器之间数格式无法兼容情况。
对于AOF存储的弊端:
- 存储数据量大,效率较低。
基于快照思想,每次读写都是全部数据,当数数据量巨大时,效率非常低。
- 大量数据下的IO性能较低
- 基于Fork创建子进程,内存产生额外消耗。
- 宕机带来数据丢失风险
解决思路(AOF的工作思想):
- 不写全数据,仅记录部分数据
- 该记录数据为记录操作过程
- 对所有操作均进行记录,排除丢失数据的风险。
AOF概念:
AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。与RDB相比可以简单描述为改记录数据位记录数据产生的过程。
AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
AOF写数据过程:
AOF写数据三种策略
- always(每次): 首次写入操作均同步到AOF文件中,数据零误差,性能较低。
- everysec(每秒): 控制了IO的速度,如果丢数据,丢一秒的数据。数据准确性高,性能高。
- no(系统控制): 由操作系统控制每次同步到AOF文件的周期,整体过程不可控。
AOF功能开启:
- 配置开关: appendonly yes
- 配置策略: appendfsync always | everysec | no
- 作用: AOF写数据策略
对conf开启支持,配置策略
启动后:
执行: set name toov5
总结: always 每执行一次修数据的指令,都记录下来。 (get不会记录的)
其他的配置:
1. appendfilename filename : 配置文件名
2.dir: 配置文件地址
对于连续重复的命令:
AOF重写(整理的一个过程):
Redis 随着命令不断的写入数据,AOF文件会越来越大。为了解决这个问题,Redis 引入了AOF重写机制压缩文件体积。AOF文件重写是将Redis进程内的数据转化为写命令同步到新AOF文件的过程。简单说就是将同一个数据的若干条命令执行结果转化成最终结果数据对应的指令进行记录。
AOF重写作用
- 降低磁盘占用量,提高磁盘利用率
- 提高持久化效率,降低持久化写时间,提高IO性能
- 降低数据恢复用时,调高数据恢复效率
AOF重写规则:
1、进程内已经超时的数据不在写入文件
2、旧的AOF文件含有无效命令。重写使用进程内数据直接和生成,这样新的的AOF文件爱只保留最终数据的写入命令。
如: del key1、 hedl key2、 srem key3、 set key4 111、 set key4 222等
3、多条写入命令可以合并成一个
如:lpush list1 a、 lpush list1 b、 lpush list1 c 可以转换为: lpush list1 a b c.
为了防止多条命令过大造成客户端缓冲溢出,对于list、set、hash、zset等类型操作,以64各元素为界限
AOF重写方式:
- 手动重写: bgrewriteaof
- 自动重写: auto-aof-rewrite-min-size size auto-aof-rewrite-percentage percentage
先set 一组数据,进行一轮操作。
然后开启重写AOF:
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
然后看到日志:
合并成了一条set
下面的操作:
127.0.0.1:6379> lpush list1 a
(integer) 1
127.0.0.1:6379> lpush list1 b
(integer) 2
127.0.0.1:6379> lpush list1 c
(integer) 3
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
查看:
[root@joe data]# cat appendonly-6379.aof
*2
$6
SELECT
$1
0
*5
$5
RPUSH
$5
list1
$1
c
$1
b
$1
a
del的那个key没有记录了。list操作也合并成了一条。重写后文件变小了
AOF手动重写-bgrewriteaof指令工作原理
和save很像:
回复的指令不一样
AOF自动重写方式(自动的一般都是基于条件的)
- 自动重写触发条件设置:
auto-aof-rewrite-min-size size
auto-aof-rewrite-percentage percent
- 自动重写触发比对参数(运行指令 info Persistence获取具体信息)
aof_current_size
aof_base_size
- 自动重写触发条件
通过 info 指令可以查看相关信息。进行比对使用。
AOF工作流程:不同的配置
关于基于everysec开启的重写: 可以通过配置去激活执行
RDB和AOF的比较:
选择建议:
对数据非常敏感,建议使用默认的AOF持久化方案
AOF持久化策略使用everysecond,每秒钟fsync一次。该策略redis仍可以保持很好的处理性能,当出 现问题时,最多丢失0-1秒内的数据。
注意:由于AOF文件存储体积较大,且恢复速度较慢
数据呈现阶段有效性,建议使用RDB持久化方案
数据可以良好的做到阶段内无丢失(该阶段是开发者或运维人员手工维护的),且恢复速度较快,阶段 点数据恢复通常采用RDB方案
注意:利用RDB实现紧凑的数据持久化会使Redis降的很低,慎重总结:
综合比对
RDB与AOF的选择实际上是在做一种权衡,每种都有利有弊
如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用AOF
如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用RDB
灾难恢复选用RDB 双保险策略,同时开启 RDB 和 AOF,重启后,Redis优先使用 AOF 来恢复数据,降低丢失数据的量
Redis事务:
Redis执行指令过程中,多条连续执行的指令被干扰,打断,插队。
客户端1 :set name zhangsan
客户端2: set name lisi
有可能被覆盖,读错了。
对于一个队列中,一次性、顺序性、排他性的执行一些列命令。
关于事务的基本操作:
- 开启事务: multi
作用: 设定事务的开启位置,这条指令执行后,后续的所有指令均加入到事务中。
- 执行事务: exec
作用: 设定事务的结束位置,同时执行事务。与multi成对出现,成对使用。
两个客户端分别set 同一个key
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 12
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> exec
1) OK
2) "12"
3) OK
4) "22"
返回了每个命令的执行结果。每个命令入队列。
注意: 加入事务的明星暂时进入到任务队列中,并没有立即执行,只有执行exec命令才开始执行。
如果在事务定义过程中出现了问题,
- 取消事务 : discard
作用: 终止当前事务的定义,发生在multi之后,exec之前
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 23
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
报错: 没有对应的 multi
定义事务的过程中,命令格式输入错误了怎么办?
1.如果定义的事务中所包含的命令存在语法错误,整体事务中所有命令均不执行。(指令没有)
2.能够正确运行的命令会执行,运行错误的命令不执行。 (用错了指令)
已经执行完毕的命令对应的数据不会自动回滚,需要开发人员在代码中实现回滚。
手动进行事务回滚:
记录凑在哦过程助攻被影响的数据之前的状态:
- 单数: string
- 多数据: hash, list, set,zset
设置指令恢复所有的被修改的项:
- 单数据: 直接set(注意周边属性,例如时效)
- 多数据: 修改对应值或整体克隆复制
关于Redis锁:
1.多个客户端有可能同时操作同一组数据,并且该数据一旦被修改后,将不适用于继续操作。
2. 在操作之前锁定要操作的数据,一旦发生变化,终止当前操作。
如何监控数据:
1.对key添加监视锁,在执行exec之前如果key发生了编号,终止事务执行。
指令: watch key1 【key2, key3....】
2. 取消对所有key的监视
指令: unwatch
127.0.0.1:6379> set name 123 OK 127.0.0.1:6379> set age 23 OK 127.0.0.1:6379> watch name OK 127.0.0.1:6379> get name "123" 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set aa bb QUEUED 127.0.0.1:6379> get aa QUEUED 127.0.0.1:6379> EXEC 1) OK 2) "bb" 继续监控 127.0.0.1:6379> watch name age OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set aa cc QUEUED 127.0.0.1:6379> get aa QUEUED 127.0.0.1:6379> EXEC 在exec执行之前,有一个客户端去修改name的值 127.0.0.1:6379> set name 8888 OK 此时客户端去执行:可以看到返回值是空 127.0.0.1:6379> EXEC (nil)
总结: watch监控的key一旦发生改变,下面定义的事务就不在执行了。
注意: 必须在开启事务之前去 watch 否则报错的。 即: 在multi之前执行。
取消:加锁与取消锁
127.0.0.1:6379> watch name OK 127.0.0.1:6379> get name "8888" 127.0.0.1:6379> UNWATCH OK
总结:
控制想操作某个数据时候,条件是别人不能动他。一旦有人动了,所有操作都取消。
业务场景: Redis状态控制的批量任务处理。
关于Redis分布式锁:
分析:
1. 使用watch监控一个key有没有改变已经不能解决问题,此处需要监控的是具体数据
2.虽然Redis是单线程的,但是多个客户端对同一数据同时进行操作时,如何避免不被同时修改?
基于特定条件的事务执行--分布式锁
使用setnx设置一个公共锁
setnx lock_key value
利用setnx命令的返回值特征,有值则返回设置失败,无值则返回设置成功。
操作完毕通过del删除掉key
127.0.0.1:6379> SETNX lock-num 1 (integer) 1 127.0.0.1:6379> incrby num -1 (integer) -1 127.0.0.1:6379> del locl-num
业务场景: 基于Redis设计分布式锁
分布式死锁问题解决:
业务场景: 依赖分布式锁的机制,某个用户操作时对应客户端宕机,且此时已经获取到锁。解决之。
分析:
- 由于锁操作由用户控制加锁解锁,必定会存在加锁后未解锁的风险。
- 需要解锁操作不仅依赖用户控制,系统级别要给出对应的保底处理方案。
127.0.0.1:6379> set name 123 OK 127.0.0.1:6379> setnx lock-name 1 (integer) 1 127.0.0.1:6379> expire lock-name 20 (integer) 1 127.0.0.1:6379> get name "123" 127.0.0.1:6379> del lock-name (integer) 1
分布式锁改良:
使用expire为锁key添加时间限定,到时不释放,放弃锁。
expire lock-key second
pexpire lock-key milliseconds
关于Redis删除策略:
1.过期数据特征: Redis是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过TTL指令获取其状态。
- XX: 具有时效性的数据
- -1: 永久有效的数据
- -2: 已经过期的数据或被删除的数据或未定义的数据
注意:
在Redis中过期id数据不会真正删除。过期数据慢慢删。还是在内存放着的。至于怎么删除,Redis是有删除策略的。
数据删除策略:
- 定时删除
- 惰性删除
- 定期删除