Redis是单线程还是多线程的?
- redis 4.0之前,是完全单线程的。
- redis 4.0时,引入了多线程,但是额外的线程也只是用于后台处理,例如:删除对象,核心流程还是完全单线程的。(核心流程指的是redis正常处理客户端请求的流程,通常包括:接收命令、解析命令、执行命令、返回结果等)
- redis6.0中,多线程主要用于网络I/O阶段,也就是接收命令和返回结果阶段,而在执行命令阶段,还是由单线程串行执行。
为什么redis是单线程?
在redis 6.0之前redis的核心操作是单线程的,因为redis是完全基于内存操作的,通常情况下CPU不会是redis的瓶颈,redis的瓶颈最有可能是机器内存的大小或者网络带宽。
既然CPU不会成为瓶颈,那就顺理成章地采用单线程方案了,因为如果使用多线程的话会更复杂,同时需要引入上下文切换、加锁等等,会带来额外的性能消耗。
redis为什么使用单进程、单线程也很快?
1、基于内存的操作,数据存在内存中,所以读写非常快,每秒可以处理超过10万次的读写操作,是已知性能最好的key-value的数据库。
2、redis使用了I/O多路复用模型,select,epoll等,基于reactor模式开发了自己的网络事件处理器
3、单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。
redis和memcached的区别:
redis
Memcached
内存高速数据库
高性能分布式内存缓存数据库,可缓存图片、视频
支持hash、list、set、zset、string结构
只支持key-value结构
将大部分数据放到内存
全部数据放到内存中
支持持久化、主从复制备份
不支持数据持久化及数据备份
数据丢失可通过AOF恢复
挂掉后,数据不可恢复
使用场景:
- 1.如果有持久化方面的需求或者对数据类型和处理有要求的应该选择redis.
- 2.如果简单的key/value存储应该选择memcached.
面试重点 redis持久化?
redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出或者宕机,服务器中的数据库状态也会消失,所以Redis提供了持久化功能。
redis提供了两种持久化机制RDB(默认)和AFO
RDB(redis database):在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时 将快照文件直接读到内存中。保存的文件是dump.rdb
redis会单独创建(fork)一个子进程来进行持久化,会先将数据读写入一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。
优点:
1、适合大规模的数据恢复。
2、对数据的完整性要不高。
缺点:
1、需要一定的时间间隔进程操作,如果redis意外宕机了,最后一次修改数据就没有了。
2、fork子进程的时候,会占用一定呢内存空间。
AOF(Append Only File):将redis执行的命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍。Aof保存的是appendonly.aof文件。
以日志的形式来记录每个写操作,将redis执行过的所有指令记录下来(读操作 不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次来完成数据的恢复工作。
- 每一次修改都同步,文件的完整性会更加好。
- 每秒同步一次,可能会丢失一秒的数据。
- 从不同步,效率是最高的。
优点:
1、数据安全,可以配置appendfsync属性,有always,每进行一次,命令操作就记录到aof文件中一次。
2、通过append模式写文件,即使中途服务器宕机(可能aof文件有错误),可以通过redis-check-aof --fix根据解决数据一致性问题。
缺点:
1、AOF文件比RDB文件大,且恢复数据慢。
2、数据集大的时候,比RDB启动效率低。
混合持久化:
混合持久化并不是一种全新的持久化方式,而是对已有方法的优化。混合持久化只发生于AOF重写过程。使用了混合持久化,重写后的新AOF文件前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。
优点:结合RDB和AOF的优点,更快的重写和恢复。
面试题: 如果同时开启RDB和AOF,服务重启时如何加载?
会先使用AOF文件来重建数据集,因为通常来说,AOF的数据会更完整。而在引入了混合持久化之后,使用AOF重建数据集时,会通过开头文件是否是“redis”来判断是否为混合持久化。
什么是缓存雪崩?什么是缓存穿透?什么是缓存击穿?解决方法?
缓存雪崩:
- 指缓存同一时间大面积的失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
- redis宕机也会产生缓存雪崩。
产比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。解决方案:
(1)redis高可用:搭建redis集群,一台挂掉之后其他的还可以继续工作。
(2)限流降级:在缓存失效时,通过加锁或者队列来控制读数据库 写缓存的线程数量。
(3)数据预热:在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间尽量均匀。
缓存穿透:(查不到)
指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
缓存穿透的概念很简单,用户想要查询一个数据,发现 redis 内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒 杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。解决方案:(1)采用 布隆过滤器布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在 控制层先进行校验,不符合则丢弃(将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉 ),从而避免了对底层存储系统的查询压力。(2)缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。
但是这种方法会存在两个问题:
1 、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;2 、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿(量太大,缓存过期)
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访 问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。这里需要注意和缓存击穿的区别,缓存击穿,是指一个key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。解决方案:
(1)设置热点数据永不过期:从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
(2)加互斥锁:分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
面试题:Redis怎么保证高可用、有哪些集群模式?
Redis保证高可用可以通过:主从复制、哨兵模式
(1)主从复制:将主库Redis的数据实时同步个从库Redis。
(2)哨兵模式(自动选取老大的模式):主从切换技术的方法是:当主服务器宕机后,需要手动吧一台服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用,这不是一种推荐的方式,更多的时候,我们优先考虑哨兵模式。redis从2.8正式提供了sentinel(哨兵)架构来解决这个问题。谋朝篡位的自动版,能够监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式的优点:
1.哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
2.主从可以切换,故障可以转移,系统的可用性就会更好
3.哨兵模式是主从模式的升级,手动到自动,更加健壮
缺点:
1、redis不好在线扩容的,集群容量一旦到达上限,在线扩容就什么麻烦。
2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择。
面试题:如何保持mysql和redis中数据的一致性?
本人刚简单地学习了一下redis,了解了它的出现背景和基本用法,对于不轻易改变的数据,首次可以将其从mysql中取出存到redis中,以后只要判断redis有没有这个数据,有的话直接拿来用就行了。那么,如果在redis获取这个数据以后,我到mysql中更新了数据,那么redis中的数据不就和mysql不一致了吗?怎么让redis中的数据和mysql保持实时一致呢?
https://www.zhihu.com/question/319817091/answer/653985863
方法:
1. Redis里的数据不立刻更新,等redis里数据自然过期。然后去DB里取,顺带重新set redis。这种用法被称作“Cache Aside”。好处是代码比较简单,坏处是会有一段时间DB和Redis里的数据不一致。这个不一致的时间取决于redis里数据设定的有效期,比如10min。但如果Redis里数据没设置有效期,这招就不灵了。
2. 更新DB时总是不直接触碰DB,而是通过代码。而代码做的显式更新DB,然后马上del掉redis里的数据。在下次取数据时,模式就恢复到了上一条说的方式。这也算是一种Cache Aside的变体。这要做的好处是,数据的一致性会比较好,一般正常情况下,数据不一致的时间会在1s以下,对于绝大部分的场景是足够了。但是有极少几率,由于更新时序,下Redis数据会和DB不一致(这个有文章解释,这里不展开)。
Cache Aside,就是“Cache”在DB访问的主流程上帮个忙
1和2的做法常规上被称为“Cache“。而且因为1有更新不及时的问题,2有极端情况下数据会不一致的问题,所以常规Cache代码会把1+2组合起来,要求Redis里的数据必须有过期时间,并且不能太长,这样即便是不一致也能混过去。同时如果是主动对数据进行更新,Cache的数据更新也会比较及时。
并且2并不一定总是行得通。比如OLTP的服务在前面是Cache+DB的模式,而数据是由后台管理系统来更新的,总是不会触碰OLTP服务,更不会动Cache。这时将Redis看作是存储也算是一种方案。就是:
3. Redis里的数据总是不过期,但是有个背景更新任务(“定时执行的代码” 或者 “被队列驱动的代码)读取db,把最新的数据塞给Redis。这种做法将Redis看作是“存储”。访问者不知道背后的实际数据源,只知道Redis是唯一可以取的数据的地方。当实际数据源更新时,背景更新任务来将数据更新到Redis。这时还是会存在Redis和实际数据源不一致的问题。如果是定时任务,最长的不一致时长就是更新任务的执行间隔;如果是用类似于队列的方式来更新,那么不一致时间取决于队列产生和消费的延迟。常用的队列(或等价物)有Redis(怎么还是Redis),Kafka,AMQ,RMQ,binglog,log文件,阿里的canal等。
Cache当作“存储”来用,访问者只看得到Cache
这种做法还有一种变体Write Through,写入时直接写DB,DB把数据更新Cache,而读取时读Cache。
Write Through + Cache当存储
以上方式无论如何都会有一段时间Redis和DB会不一致。实践上,这个不一致时间短则几十ms,长可以到几十分钟。这种程度的一致性对于很多业务场景都已经足够了。很多时候,用户无法区分自己读取的是Redis还是DB,只能读取到其中的一个。这时数据看起来直觉上是没问题的就可以接受了。只要不出现,用户先看见了数据是A,然后看到数据是B,之后一刷新,又看到A的尴尬场景就行了。(这也可以部份解释为啥用经常使用共享式的Cache而不是本地Cache方案)。
但对于有些业务,比如协作文档编辑,电商秒杀的扣库存,银行转账等,以上的做法就不够用了。解决办法也有两大类。第一种是不要用Redis,只用DB。或者更直接点说是“只要一个单点的数据源”。这样肯定就没有一致性问题,代价就是CAP中因为CP被满足,因此A被牺牲掉。这就是为啥银行一系统升级就要停服务的原因。
当然实际上也有CAP兼顾,但是C要的强一点,A就得弱一点,但不至于完全牺牲掉的做法。这里不展开。
另外一种保证一致性的做法就是用某种分布式协议一致性来做,大致可以归结到
这些做法实施复杂度和运维复杂度太高,以至于对于像Redis + DB这种场景基本上没人这么干。本质上大家用Redis一般也就是想做个Cache而已。这些方案通常被用到比如多数据中心数据一致性维护的系统中。
综上,除了单点DB存储之外的方案,其一致性面临的窘境是
最最后提醒下,本文有很多不严谨的地方,包括对Cache的形式总结其实只有典型的几种,实际可能的要多得多;再比如对一致性的介绍也非常粗浅,原因是为了让初学者有一点点概念,能看得进去(就这样,已经很长了,评论区里也有人表示接受不了)。
对于分布式和其一致性的完整知识的学习需要耗费大量的精力,Good Luck & Best Wishes。
redis传统五大数据类型的落地应用?
备注:
应用:
1.string
2.hash
3.list
4.set set 不可重复,无序
(1)微信抽奖小程序
(2)微信朋友圈点赞
(3)微博好友关注社交关系
差集
5.zset
(1)根据商品销售对商品进行排序显示
(2)抖音热搜
Zset底层实现?跳表搜索插入删除过程?
什么是跳表:https://zhuanlan.zhihu.com/p/53975333
跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度
Zset数据量少的时候使用压缩链表ziplist实现,有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。数据量大的时候使用跳跃列表skiplist和哈希表hashMap结合实现,查找删除插入的时间复杂度都是O(longN)
搜索
跳跃表按 score 从小到大保存所有集合元素,查找时间复杂度为平均 O(logN),最坏 O(N) 。
插入
之前就说了,之所以选用链表作为底层结构支持,也是为了高效地动态增删。单链表在知道删除的节点是谁时,时间复杂度为O(1),因为跳表底层的单链表是有序的,为了维护这种有序性,在插入前需要遍历链表,找到该插入的位置,单链表遍历查找的时间复杂度是O(n),同理可得,跳表的遍历也是需要遍历索引数,所以是O(logn)。
删除
删除的节点要分两种情况,如果该节点还在索引中,那删除时不仅要删除单链表中的节点,还要删除索引中的节点;另一种情况是删除的节点只在链表中,不在索引中,那只需要删除链表中的节点即可。但针对单链表来说,删除时都需要拿到前驱节点才可改变引用关系从而删除目标节点。
知道分布式锁吗?有哪些实现方案?你谈谈对redis分布式锁的理解,删key的时候有什么问题?
首先要区别synchorized,lock...等jvm层面的锁和分布式锁的差别。
分布式微服务架构,拆分之后各个微服务之间为了避免冲突和数据故障而加入的一种锁,就叫分布式锁。
为什么分布式系统中不能用普通锁呢?那么普通锁和分布式锁有什么区别呢?
普通锁:单一系统中,同一个应用程序是有同一个进程,然后多个线程并发会造成数据安全问题,他们是共享同一块内存的,所以在内存某个地方做标记即可满足需求,例如synchronized和volatile+cas一样对具体的代码做标记,对应的就是在同一块内存区域作了同步的标记。
分布式锁:分布式系统中,最大的区别就是不同系统中的应用程序都是在各自机器上不同的进程中处理的,这里的线程不安全可以理解为多进程造成的数据安全问题,他们不会共享同一台机器的同一块内存区域,因此需要将标记存储在所有进程都能看到的地方。例如zookeeper作分布式锁,就是将锁标记存储在多个进程共同看到的地方,redis作分布式锁,是将其标记公共内存,而不是某个进程分配的区域。
实现方案:mysql (用的最少) zookeeper(用的最多) redis(用的次之)
a:zookeeper实现分布式锁
实现方式:
方案1:利用节点名称的唯一性来实现共享锁。
算法思路: 利用名称唯一性,加锁操作时,只需要所有客户端一起创建/test/Lock节点,只有一个创建成功,成功者获得锁。解锁时,只需删除/test/Lock节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁。
方案2:利用临时顺序节点实现共享锁。(主要是用这种方式实现)
算法思路:对于加锁操作,可以让所有客户端都去/lock目录下创建临时顺序节点,如果创建的客户端发现自身创建节点序列号是/lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点),进入等待。
比如创建节点:/lock/0000000001、/lock/0000000002、/lock/0000000003。则节点/lock/0000000001会先获得锁,因为zk上的节点是有序的,且都是最小的节点先获得锁。
注:临时顺序节点比持久顺序节点的好处是:当zookeeper宕机后,临时顺序节点会自动删除,获取锁的客户端会释放锁,不会一直造成锁等待,而持久节点会造成锁等待。
两种方式的区别:
方案1会产生惊群效应:假如许多客户端在等待一把锁,当锁释放时候所有客户端都被唤醒,然后竞争分布式锁,仅仅有一个客户端得到锁。
方案2是按照创建顺序排队的实现,多个客户端共同等待锁,当锁释放时只有一个客户端会被唤醒,在zk上注册节点最小的客户端会被唤醒,避免了惊群效应。b:redis实现分布式锁
redis实现分布式锁主要靠四个命令:
setnx(set if not exits 维护着是乐观锁):当不存在key的时候,才为key设置值为value。setnx与set的区别:set是存在key,则去覆盖value;setnx是不存在key,则重新给key和value赋值。
getset:根据key得到旧的值,并set新的值。
expire:设置过期时间。
del:删除实现方式:
1:获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
2:获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
3:释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。c:数据库实现分布式锁(用的最少)
实现方式:利用的是乐观锁和悲观锁
乐观锁:在表中添加版本号的字段,每次更新前都先查询出带版本号的数据,然后再更新的时候where条件语句后带版本号条件,更新成功表示锁已占用,更新不成功表示锁没被占用。
悲观锁:利用select...for update(X锁)/select...lock in share mode(S锁),一般来说用X锁的较多,因为后续多会做写功能的实现。
注:当实现悲观锁的时候,需要关闭数据库的事务自动提交机制不然不会生效。因此java代码中应该选择主动关闭数据库的事务自动提交功能。
一般的互联网公司,大家都习惯用redis做分布式锁
redis---redlock (分布式锁)====> rediosson lock/unlock
常见的面试题:
- Redis除了拿来做缓存,你还见过基于Redis的什么用法?
- Redis做分布式锁的时候有需要注意的问题?
- 如果是Redis是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
- 集群模式下,比如主从模式,有没有什么问题呢?
- 那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈。
- Redis分布式锁如何续期?看门狗知道吗?
使用场景:多个服务间 + 保证同一时刻内 + 同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)
建两个Module:boot_redis01,boot_redis02
POM
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.3.RELEASE
com.lun
boot_redis01
1.0.0-SNAPSHOT
jar
boot_redis01
http://maven.apache.org
UTF-8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
redis.clients
jedis
3.1.0
org.springframework.boot
spring-boot-starter-aop
org.redisson
redisson
3.13.4
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-maven-plugin
Properties
server.port=1111
#2222
#=========================redis相关配香========================
#Redis数据库索引(默认方0)
spring.redis.database=0
#Redis服务器地址
spring.redis.host=192.168.111.147
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默犬认0
spring.redis.lettuce.pool.min-idle=0
主启动类
@SpringBootApplication
public class BootRedis01Application{
public static void main(String[] args){
SpringApplication.run(BootRedis01Application.class, args);
}
}
配置类
import java.io.Serializable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory){
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
controller
@RestController
public class GoodController{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String result = stringRedisTemplate.opsForValue().get("goods:001");// get key ====看看库存的数量够不够
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "\t服务提供端口" + serverPort);
return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
}else{
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
}
}
测试
redis:set goods:001 100
浏览器:http://localhost:1111/buy_goods
boot_redis02拷贝boot_redis01
JVM层面的加锁,单机版的锁
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds//不见不散
try {
// ... method body
} finally {
lock.unlock()
}
}
public void m2() {
if(lock.tryLock(timeout, unit)){//过时不候
try {
// ... method body
} finally {
lock.unlock()
}
}else{
// perform alternative actions
}
}
}
分布式部署后,单机锁还是出现超卖现象,需要分布式锁
Nginx配置负载均衡,Nginx学习笔记or备份
Nginx配置文件修改内容
upstream myserver{
server 127.0.0.1:1111;
server 127.0.0.1:2222;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
# 负责用到的配置
proxy_pass http://myserver;
root html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
启动两个微服务:1111,2222,多次访问http://localhost/buy_goods,服务提供端口在1111,2222两者之间横跳
上面手点,下面高并发模拟
redis:set goods:001 100,恢复到100
用到Apache JMeter,100个线程同时访问http://localhost/buy_goods。
启动测试,后台打印如下:
这就是所谓分布式部署后出现超卖现象。
Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理。
SET
在Java层面
public static final String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void m(){
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
if(!flag) {
return "抢锁失败";
}
...//业务逻辑
stringRedisTemplate.delete(REDIS_LOCK);
}
上面Java源码分布式锁问题:出现异常的话,可能无法释放锁,必须要在代码层面finally释放锁。
解决方法:try…finally…
public static final String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void m(){
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
if(!flag) {
return "抢锁失败";
}
...//业务逻辑
}finally{
stringRedisTemplate.delete(REDIS_LOCK);
}
}
另一个问题:部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key。
public static final String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void m(){
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
//设定时间
stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
if(!flag) {
return "抢锁失败";
}
...//业务逻辑
}finally{
stringRedisTemplate.delete(REDIS_LOCK);
}
}
新问题:设置key+过期时间分开了,必须要合并成一行具备原子性。
解决方法:
public static final String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void m(){
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try{
Boolean flag = stringRedisTemplate.opsForValue()//使用另一个带有设置超时操作的方法
.setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
//设定时间
//stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
if(!flag) {
return "抢锁失败";
}
...//业务逻辑
}finally{
stringRedisTemplate.delete(REDIS_LOCK);
}
}
另一个新问题:张冠李戴,删除了别人的锁
解决方法:只能自己删除自己的,不许动别人的。
public static final String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void m(){
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try{
Boolean flag = stringRedisTemplate.opsForValue()//使用另一个带有设置超时操作的方法
.setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
//设定时间
//stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
if(!flag) {
return "抢锁失败";
}
...//业务逻辑
}finally{
if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)) {
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}
finally块的判断 + del删除操作不是原子性的
Redis事务复习,Redis学习笔记
事务介绍
继续上一章节,解决之道
法一:事务的方法:
public static final String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void m(){
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try{
Boolean flag = stringRedisTemplate.opsForValue()//使用另一个带有设置超时操作的方法
.setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
//设定时间
//stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
if(!flag) {
return "抢锁失败";
}
...//业务逻辑
}finally{
while(true){
stringRedisTemplate.watch(REDIS_LOCK);
if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(REDIS_LOCK);
List
法二: redis调用lua脚本通过eval命令保证代码执行的原子性
RedisUtils:
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisUtils {
private static JedisPool jedisPool;
static {
JedisPoolConfig jpc = new JedisPoolConfig();
jpc.setMaxTotal(20);
jpc.setMaxIdle(10);
jedisPool = new JedisPool(jpc);
}
public static JedisPool getJedis() throws Exception{
if(jedisPool == null)
throw new NullPointerException("JedisPool is not OK.");
return jedisPool;
}
}
public static final String REDIS_LOCK = "redis_lock";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void m(){
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try{
Boolean flag = stringRedisTemplate.opsForValue()//使用另一个带有设置超时操作的方法
.setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
//设定时间
//stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
if(!flag) {
return "抢锁失败";
}
...//业务逻辑
}finally{
Jedis jedis = RedisUtils.getJedis();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] "
+ "then "
+ " return redis.call('del', KEYS[1]) "
+ "else "
+ " return 0 "
+ "end";
try {
Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK),//
Collections.singletonList(value));
if("1".equals(o.toString())) {
System.out.println("---del redis lock ok.");
}else {
System.out.println("---del redis lock error.");
}
}finally {
if(jedis != null)
jedis.close();
}
}
}
确保RedisLock过期时间大于业务执行时间的问题
Redis分布式锁如何续期?
集群 + CAP对比ZooKeeper ,重点,CAP
CAP
C:Consistency(强一致性)
A:Availability(可用性)
P:Partition tolerance(分区容错性)
综上所述
Redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现。
Redisson官方网站
Redisson配置类
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
}
Redisson模板
public static final String REDIS_LOCK = "REDIS_LOCK";
@Autowired
private Redisson redisson;
@GetMapping("/doSomething")
public String doSomething(){
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try {
//doSomething
}finally {
redissonLock.unlock();
}
}
回到实例
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GoodController{
public static final String REDIS_LOCK = "REDIS_LOCK";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods(){
//String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try {
String result = stringRedisTemplate.opsForValue().get("goods:001");// get key ====看看库存的数量够不够
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "\t服务提供端口" + serverPort);
return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
}else{
System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
}
return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
}finally {
redissonLock.unlock();
}
}
}
重启boot_redis01,boot_redis02,Nginx,重置Redis:set goods:001 100
,启动JMeter,100个线程访问http://localhost/buy_goods。最后,后台输出:
让代码更加严谨
public static final String REDIS_LOCK = "REDIS_LOCK";
@Autowired
private Redisson redisson;
@GetMapping("/doSomething")
public String doSomething(){
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try {
//doSomething
}finally {
//添加后,更保险
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
}
可避免如下异常:
IllegalMonitorStateException: attempt to unlock lock,not loked by current thread by node id:da6385f-81a5-4e6c-b8c0
配置文件redis.conf的maxmemory参数,maxmemory是bytes字节类型,注意转换
redis默认内存多少可以用?
如果不设置最大内存或者最大内存设置为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存
一般生产上如何配置?
一般推荐redis设置内存为最大物理内存的3/4
如何修改redis内存设置?
maxmemory 104857600
什么命令查看redis内存使用情况?
info memeory
如果redis内存使用超出了设置的最大值会怎样?
我改改配置,故意把最大值设为1B
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> config set maxmemory 1
OK
127.0.0.1:6379> set a 123
(error) OOM command not allowed when used memory > 'maxmemory'.
没有加上过期时间就会导致数据写满maxmemory,会出现oom错误,为了避免类似情况,引出内存淘汰策略。
redis的内存淘汰策略?
往redis里写的数据是怎么没了的?redis过期键的删除策略?
如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被删除呢?
答案:不是!!
三种不同的删除策略:
定时删除
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。
这会产生大量的性能消耗,同时也会影响数据的读取操作。
惰性删除
数据到达过期时间,不做处理。等下次访问该数据时,
如果未过期,返回数据;
发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。
如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏 – 无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息。
定期删除
定期删除策略是前两种策略的折中:
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
周期性轮询Redis库中的时效性数据,来用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间(随机抽查,重点抽查)
举例:
redis默认每隔100ms检查,是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
上述步骤都过堂了,还有漏洞吗?
定期删除时,从来没有被抽查到
惰性删除时,也从来没有被点中使用过
上述2步骤====>大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽
必须要有一个更好的兜底方案
内存淘汰策略登场(Redis 6.0.8版本)
上面总结
如何配置,修改
redis的LRU了解过吗?可否手写一个LRU算法?
是什么
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。
算法来源
https://leetcode-cn.com/problems/lru-cache/
设计思想
查找快、插入快、删除快,且还需要先后排序---------->什么样的数据结构可以满足这个问题?
你是否可以在O(1)时间复杂度内完成这两种操作?
如果一次就可以找到,你觉得什么数据结构最合适?
答案:LRU的算法核心是 哈希+双向链表
编码手写如何实现LRU
本质就是HashMap + DoubleLinkedList
时间复杂度是O(1),哈希表+双向链表的结合体
方法一:巧用LinkedHashMap完成LRU算法
import java.util.LinkedHashMap;
public class LRUCache {
private LinkedHashMap cache;
public LRUCache(int capacity) {
cache = new LinkedHashMap(capacity, 0.75f, true){
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(java.util.Map.Entry eldest) {
return size() > capacity;
}
};
}
public int get(int key) {
return cache.getOrDefault(key, -1);
}
public void put(int key, int value) {
cache.put(key, value);
}
@Override
public String toString() {
return cache.toString();
}
}
方法二:哈希表+双向链表
class LRUCache2{
class Node{//双向链表节点
K key;
V value;
Node prev;
Node next;
public Node() {
this.prev = this.next = null;
}
public Node(K key, V value) {
super();
this.key = key;
this.value = value;
}
}
//新的插入头部,旧的从尾部移除
class DoublyLinkedList{
Node head;
Node tail;
public DoublyLinkedList() {
//头尾哨兵节点
this.head = new Node();
this.tail = new Node();
this.head.next = this.tail;
this.tail.prev = this.head;
}
public void addHead(Node node) {
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
}
public void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
node.prev = null;
node.next = null;
}
public Node getLast() {
if(this.tail.prev == this.head)
return null;
return this.tail.prev;
}
}
private int cacheSize;
private Map> map;
private DoublyLinkedList doublyLinkedList;
public LRUCache2(int cacheSize) {
this.cacheSize = cacheSize;
map = new HashMap<>();
doublyLinkedList = new DoublyLinkedList<>();
}
public int get(int key) {
if(!map.containsKey(key)) {
return -1;
}
Node node = map.get(key);
//更新节点位置,将节点移置链表头
doublyLinkedList.removeNode(node);
doublyLinkedList.addHead(node);
return node.value;
}
public void put(int key, int value) {
if(map.containsKey(key)) {
Node node = map.get(key);
node.value = value;
map.put(key, node);
doublyLinkedList.removeNode(node);
doublyLinkedList.addHead(node);
}else {
if(map.size() == cacheSize) {//已达到最大容量了,把旧的移除,让新的进来
Node lastNode = doublyLinkedList.getLast();
map.remove(lastNode.key);//node.key主要用处,反向连接map
doublyLinkedList.removeNode(lastNode);
}
Node newNode = new Node<>(key, value);
map.put(key, newNode);
doublyLinkedList.addHead(newNode);
}
}
}
添加节点:
删除节点:
Redis事务?
Redis事务没有隔离级别的概念!
Redis单条命令是保证原子性的,但是事务不保证原子性。
所有的命令在事务中,并没有被直接执行!只有当exec命令被执行时才会执行。
案例1:当事务中的命令出现编译型异常(代码有问题!命令有错!),事务中所有的命令都不会执行!
案例2:如果事务队列中存在运行时异常(1/0),那么执行命令的时候,其他命令是可以执行的,错误命令抛出异常。
什么是Jedis?
Jedis是Redis官方推荐的java连接开发工具!使用java操作Redis中间件。
1.导入依赖:
import redis.clients.jedis.Jedis;
public class TestPing {
public static void main(String[] args) {
// 1、 new Jedis 对象即可
Jedis jedis = new Jedis("127.0.0.1",6379);
// jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要!
System.out.println(jedis.ping());
}
}