JVM
JVM是Java virtual machine(Java虚拟机)的缩写,是一种用于计算机的规范,是通过在实际计算机上仿真模拟各种计算机功能来实现的。
主要组件构成:
1.类加载器
子系统负责从文件系统或者网络中加载Class文件,Class文件在文件开头有特定的文件标识,ClassLoader只负责加载Class文件,是否运行取决于ExecutionEngine。
2.执行引擎
执行字节码或者执行本地方法
3.内存管理系统
负责管理Java程序运行时的内存分配和回收,包括堆、方法区等。
4.即时编译器
将频繁执行的字节码转换为本地机器代码,以提高执行效率。
5.运行时数据区
包括方法区、堆、Java栈、程序计数器、本地方法栈。
jvm的内存结构:堆内存、方法区、栈
1.堆内存(heap)
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
2.方法区(Method Area)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
3、程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
4、Java虚拟机栈(JVM Stacks)
Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
5、本地方法栈(Native Method Stacks)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
JVM内存分配机制
JVM内存≈Heap(堆内存)+PermGen(方法区)+Thrend(栈) Heap(堆内存)=Young(年轻代)+Old(老年代),官方文档建议整个年轻代占整个堆内存的3/8,老年代占整个堆内存的5/8,但是可以配置为其他比例。 Young(年轻代)=EdenSpace+FromSurvivor+ToSurvivor,Eden区与两个存活区的内存大小比例是:8:1:1,同样可以配置为其他比例。
java代码如何被JVM执行的?
java文件计算机是识别不了的,所以首先我们写的代码需要从.java文件编译为.class文件,.class是字节码文件,里面都是一些字节码指令,这样的字节码指令才是计算机可以识别的。
JVM垃圾回收机制
1、new出来的对象先放在Eden区,Eden区放满后第一次触发Young GC(垃圾回收),把存活对象移到S1存活区。
2、第二次Eden区又满了,再次触发Young GC,把Eden区的存活对象移到S2存活区,把S1存活区的存活对象也移到S2存活区,这时S1存活区清空了。
3、第三次Eden区又满了,再次触发Young GC,把Eden区的存活对象移到S1存活区,把S2存活区的存活对象也移到S1存活区,这时S2存活区清空了。
4、这样S1和S2交替互换,轮流为清空,大大拉长了存活对象进入老年代的时间间隔。
类对象什么时候进入老年代:
a、大对象直接进入老年代:Eden区放不下直接进入老年代
b、长期存活的对象进入老年代:以Young GC次数进行判断的,默认次数15次后进入老年代
c、执行Young GC时,存活区放不下时,存活对象也直接进入老年代
5、一直这样循环往复直到老年代满了,触发Full GC。首先清除老年代中的没有引用的对象,再对Eden区进行GC,还会对持久代进行GC(持久代一般没什么可清理)
6、老年代里面放满以后,执行Full GC也释放不了内存空间,就会报内存溢出的错误了。
总结:
1、Young GC只发生在Eden区,Eden区是整个Java堆内存分配的入口,new对象优先分配到Eden区,Eden区满之后触发Young GC
2、Young GC触发后,然后它会判断Eden区的对象是否是存活的,如果是存活的则放到存活区,不是存活的则清除掉释放内存空间。
3、触发Full GC是虽然也清理了Eden区,但是Young GC次数不会+1,它是Full GC在干活。
什么时候触发Full GC:
a、老年代空间不足
b、持久代空间不足的时候也会触发Full GC
c、显示调用也可以触发Full GC,比如说RunTime.GC、System.GC
d、RMI框架,会产生大量的对象,会进行显示调用,触发Full GC
e、Young GC时的悲观策略dump live的内存信息时(jmap-dump:live)
4、执行Young GC和Full GC应用程序的所有线程都是暂停的、停止工作,但Full GC时间比较长 5、JVM调优的核心思想:
a、尽量减少Full GC的次数,或者说延长Full GC间隔时间。不要频繁触发Full GC,因为执行Full GC的时间比较长。
b、尽量减少Young GC执行的时间
回收算法
标记-清除算法
分为两个阶段:标记和清除,内存使用了一段时间之后,里面肯定有垃圾对象也有正常对象,第一个阶段进行标记,就是把每个对象标记出是垃圾对象还是正常对象,而具体判断依据就是上面的垃圾判断算法;第二个阶段进行清除,就是把所有垃圾对象进行清除:
标记-清除算法适用于存活对象比较多的情况,清除的对象越少效率就高,缺点是清除之后会产生内存碎片,随着多次垃圾回收,多次标记-清除之后,内存碎片现象会更加严重,导致大对象找不到连续内存进行存放。
标记-复制算法
复制算法就是说有把内存划分为两块,A块和B块,第一次使用时比如说使用了B块,就会只在B块中分配对象,当进行垃圾回收的时候,把B块中存活的对象全部复制出来,然后放在A块里面,清空B块;垃圾回收之后,进行第二次使用时,使用A块,依次往复:
复制算法的适用于存活对象比较少的情况,复制的对象越少效率就高,缺点就是内存使用率不高,因为始终有一块内存是闲置的。
标记-整理算法
分为两个阶段:标记和整理,内存使用了一段时间之后,里面有垃圾对象和正常对象,第一个阶段进行标记,和标记-清除算法中的标记过程一样,把每个对象标记出来是垃圾对象还是正常对象,具体判断依据就是上面的垃圾判断算法;第二个阶段进行整理,就是把垃圾对象清理之后,并且把存活对象进行内存整理,整齐地在内存中排列:
标记-整理算法同样适用于存活对象比较多的情况,清除的对象越少效率就越高,而且通过内存整理解决了内存碎片的问题,但同时也意味着更慢了,因为需要时间来进行内存整理。
分代收集算法
我们将内存区域分成两片,一边叫新生代,另一边叫老年代
并且引入一个属性:年龄
我们将年龄定义为:经历过多少次 GC,且还存活的轮次,例如,经历 3 次 GC,还健在的对象年龄为 3
新生代再分为伊甸区和两个幸存区,然后每次新建立的对象都会放在伊甸区,并且根据经验规律,大部分对象连一轮 GC 都撑不过,所以每次 GC 后,伊甸区只会剩下小部分对象
然后将这部分对象通过复制算法移动到其中一个幸存区中,再释放伊甸区的对象(注意:幸存区有两个,可以方便复制算法在这两个区域运行)
移动到幸存区后,还会经历多次 GC,每次 GC 运行后,幸存的对象又会通过复制算法移动到另一个幸存区,再将之前的幸存区释放
随后,将幸存区中满足年龄要求的对象会通过复制算法移动到老年代
能够到达老年区也就足以说明这些对象在短时间内还死不了,所以老年代中会进行频率更低的 GC,如果老年代中发现了垃圾,就会通过标记-整理算法清除。(如果对象空间很大,则会被直接移动到老年代中)
垃圾收集器,问什么时候下会触发gc
GC是由JVM自动完成的,根据JVM系统环境而定,所以机制是不确定的,当然我们也可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行垃圾回收,但是具体什么时候运行也是无法控制的,也就是说我们调用了System.gc()只是通知JVM要去回收,但是什么时候回收是由JVM决定的,但是不建议手动调用该方法,因为GC消耗的资源比较大。
那会触发垃圾回收的情况有以下几种:
1.当Eden区或者S区不够用了
2.老年代空间不够用了
3.方法区空间不够用了
4.System.gc() 执行的是Full GC的操作
参考:垃圾回收(GC) 很干,很全_gc垃圾回收_浩展的博客-CSDN博客
垃圾回收机制——GC详讲_gc回收机制_答辣喇叭的博客-CSDN博客
Redis
redis是一款高性能的NOSQL系列的非关系型数据库, NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,是一项全新的数据库理念,泛指非关系型的数据库。
Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供测试数据,50个并发执行100000个请求,读的速度是110000次/s,写的速度是81000次/s ,且Redis通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:
1) 字符串类型 string
2) 哈希类型 hash
3) 列表类型 list
4) 集合类型 set
5) 有序集合类型 sortedset
redis的数据结构
redis存储的是:key,value格式的数据,其中key都是字符串,value有5种不同的数据结构
value的数据结构:
1) 字符串类型 string
2) 哈希类型 hash : map格式
3) 列表类型 list : linkedlist格式。支持重复元素
4) 集合类型 set : 不允许重复元素
5) 有序集合类型 sortedset:不允许重复元素,且元素有顺序
Redis优缺点
优点
读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化,支持AOF和RDB两种持久化方式。
支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点
数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
redis 五种数据结构对应的数据场景
1.String 字符串类型
String是redis中最基本的数据类型,一个key对应一个value。
String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
这里稍微讲一下什么是二进制安全,一个二进制安全功能(函数),其本质上将操作输入作为原始的、无任何特殊格式意义的数据流。对于每个字符都公平对待,不特殊处理某一个字符。
什么意思呢,举个例子:
C语言中的字符串是根据特殊字符“\0”来判断该字符串是否结束,对于字符串str="0123456789\0123456789”来说,在C语言里面str的长度就是10(strlen(str)=10),所以strlen()函数不是二进制安全的。而在Redis中,strlen str的结果是21,是二进制安全的(Redis底层所使用的字符串表示是Sds),它只关心二进制化的字符串,不关心字符串的具体格式,里面有啥字符,只会严格的按照二进制的数据存取,不会以某种特殊格式解析字符串。
实战场景:
1.缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
举个我曾经做看守所项目时候使用过的例子:做很多流程操作的时候比如家属会见、财务管理等等我们往往都会先判断当前人员是否为在押人员,而判断是否为在押人员是另外一个微服务接口,如果直接实时的去调用那个接口,短时的高并发很有可能把这个服务也拖挂,最终导致整个系统不可用,并且 RPC 本身也是比较耗时的,所以就考虑在这里进行优化。
那当时我们是怎么做呢?很简单的一个思路,提前将所有的在押人员的user_id存到redis中,这样,当请求到来的时候我们直接通过缓存可以快速判断是否为在押人员。如果不是则直接在这里返回前端。
通过预先处理减少了实时链路上的 RPC 调用,既减少了系统的外部依赖,也极大的提高了系统的吞吐量。
2.计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。一般会使用decr、incr命令用于计数器的实现。
当遇到需求,在规定时间,用户的访问量不能超过规定次数的时候就可以用redis中的计数器来实现了又可以使用这个技术用来做限流(使用用户的ip作为key,用户访问一次,就加1,如果超过次数就返回false)可以处理业务上面的的一些访问次数之类的,例如:文章的点赞数,阅读量,允许有一点的延迟效果,先保存到redis中,然后在同步到数据库当中3.session:常见方案spring session + redis实现session共享
2.Hash (哈希)
Hash是一个Mapmap,Value值本身又是一种键值对结构,如 value={{field1,value1},…fieldN,valueN}}
实战场景:
1.缓存: 能直观,相比string更节省空间一些,可以维护缓存信息,如用户信息,视频信息等,但用hash实现的,string也可以实现。
为什么这么说呢?
其实hash类型的(key、field、value)的结构与对象的(对象id,属性,值)的结构相似,也可以用来存储对象。
所以hash也可以用string+json存储对象的一种方式,那么存储对象时,到底用string+json还是用hash呢?
string+json | hash | |
效率 | 很高 | 高 |
容量 | 低 | 低 |
灵活性 | 低 | 高 |
序列化 | 简单 | 复杂 |
所以说,当对象的某个属性需要频繁修改时,不适合用string+json,因为不够灵活,每次修改都需要重新将整个对象序列化并赋值,如果使用hash类型,则可以针对某个属性单独修改,没有序列化,也不需要修改整个对象。比如,商品的价格、销量、关注数、评论数等可能经常发生变化的属性,就适合存储在hash类型里面。
综上,一般对象用string+json存储,对象中某些频繁变化的属性抽出来用hash存储。
2.购物车:以用户id为key,商品id为field,商品数量为value,恰好构成购物车的3个要素。
3.链表
List 说白了就是链表(redis 使用双端链表实现的 List),是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。
实战场景:
1.timeline:例如微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。
2.排行榜
list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中,如京东每日的手机销量排行、学校每次月考学生的成绩排名。但是,并不是所有的排行榜都能用list类型实现,只有定时计算的排行榜才适合使用list类型存储,与定时计算的排行榜相对应的是实时计算的排行榜,list类型不能支持实时计算的排行榜
但是,对于频繁更新的列表,list类型的分页可能导致列表元素重复或漏掉。
举个例子,当前列表里由表头到表尾依次有(E,D,C,B,A)五个元素,每页获取3个元素,用户第一次获取到(E,D,C)三个元素,然后表头新增了一个元素F,列表变成了(F,E,D,C,B,A),此时用户取第二页拿到(C,B,A),元素C重复了。只有不需要分页(比如每次都只取列表的前5个元素)或者更新频率低(比如每天凌晨更新一次)的列表才适合用list类型实现。对于需要分页并且会频繁更新的列表,需用使用有序集合sorted set类型实现。另外,需要通过时间范围查找的最新列表,list类型也实现不了,也需要通过有序集合sorted set类型实现,如以成交时间范围作为条件来查询的订单列表。
4.Set 集合
集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中 1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。跟Java的HashSet类似。
实战场景:
1.标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
2.点赞,或点踩,收藏等,可以放到set中实现
因为Redis为set类型提供了求交集,并集,差集的操作,可以非常方便地实现譬如共同关注、共同爱好、共同好友等功能。
sinter交集命令可以获得A和B两个用户的共同好友sismember命令可以判断A是否是B的好友scard命令可以获取好友数量关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合srandmember命令可以随机展示当然黑名单白名单也一样,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中。
5.zset 有序集合
有序集合和集合有着必然的联系,它和set一样是不可重复的,区别在于多了score值,用来代表排序的权重。也就是当你需要一个有序的,不可重复的集合列表时,就可以考虑使用这种数据类型。
(有序集合中的元素不可以重复,但是score 分数可以重复,就和一个班里的同学学号不能重复,但考试成绩可以相同)。
实战场景:
1.排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
使用redis做缓存的原因
高性能:
redis是用C语言编写的,稳定性和性能更好。
用户第一次访问数据库中的某些数据,因为是从硬盘上读取的,这个过程会比较慢。将该用户访问的数据存在redis缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高可靠:
支持集群模式和持久化等特性,不会应为缓存量太多而导致虚拟机崩溃。Redis是独立部署的,即使网站更新,redis缓存的数据也不会消失。
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
redis 高性能原理的原因
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,
HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
合理的数据编码
对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。
那我们就来看看,不同的数据类型是如何进行编码转化的:
String:存储数字的话,采用int类型的编码,如果是非数字的话,采用 raw 编码;
List:字符串长度及元素个数小于一定范围使用 ziplist 编码,任意条件不满足,则转化为 linkedlist 编码;
Hash:hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对;
Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码;
Zset:zset 对象中保存的元素个数小于及成员长度小于一定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码。
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换
而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的
性能消耗;
4、使用多路 I/O 复用模型,非阻塞 IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis
直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请
求;
redis 缓存 出现的缓存击穿、缓存雪崩、缓存穿透的具体解决方法
三者出现的根本原因:Redis命中率下降,请求直接打在DB上
正常情况下,大量的资源请求都会被redis响应,在redis得不到响应的小部分请求才会去请求DB,这样DB的压力是非常小的,是可以正常工作的(如下图):
如果大量的请求在redis上得不到响应,那么就会导致这些请求会直接去访问DB,导致DB的压力瞬间变大而卡死或者宕机。如下图:
① 大量的高并发的请求打在redis上
② 这些请求发现redis上并没有需要请求的资源,redis命中率降低
③ 因此这些大量的高并发请求转向DB(数据库服务器)请求对应的资源
④ DB压力瞬间增大,直接将DB打垮,进而引发一系列“灾害”
那么为什么redis会没有需要访问的数据呢?通过分析大致可以总结为三种情况,也就对应着redis的雪崩、穿透和击穿(下文开始进行详解)
问题名称 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
资源是否存在DB数据库服务器中 | × | √ | √ |
资源是否存在Redis中 | × | × | × |
redis没有对应资源的原因 | 根本不存在该资源(DB也没有) | 某个热点key过期 | 大部分key集体过期 |
根本原因: 大量的高并发的请求打在Redis上,但是发现Redis中并没有请求的数据,redis的命令率降低,所以这些请求就只能直接打在DB(数据库服务器)上,在大量的高并发的请求下就会导致DB直接卡死、宕机。
1.缓存穿透
缓存穿透产生的原因:请求根本不存在的资源(DB本身就不存在,Redis更是不存在)这时的用户很可能是攻击者,如发起为id为"-1"的数据或id为特别大(不存在的数据),导致数据库压力过大或宕机。
解决方式:
对空值进行缓存:
类似于上面的例子,虽然数据库中没有id=-3872的用户的数据,但是在redis中对他进行缓存(key=-3872,value=null),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上
实时监控:
对redis进行实时监控,当发现redis中的命中率下降的时候进行原因的排查,配合运维人员对访问对象和访问数据进行分析查询,从而进行黑名单的设置限制服务(拒绝黑客攻击)
使用布隆过滤器:
使用BitMap作为布隆过滤器,将目前所有可以访问到的资源通过简单的映射关系放入到布隆过滤器中(哈希计算),当一个请求来临的时候先进行布隆过滤器的判断,如果有那么才进行放行,否则就直接拦截
接口校验:
类似于用户权限的拦截,对于id=-3872这些无效访问就直接拦截,不允许这些请求到达Redis、DB上。
注意事项:
1).使用空值作为缓存的时候,key设置的过期时间不能太长,防止占用太多redis资源
2).使用空值作为缓存只能防止黑客重复使用相同的id暴力攻击,但是如果黑客使用动态的无效id攻击就没有效果(需要配合网警)
3).使用布隆过滤器也是有哈希冲突的可能
2.缓存雪崩
缓存雪崩产生的原因:redis中大量的key集体过期, 缓存集中过度,或者缓存服务器宕机,导致大量请求访问数据库,造成数据库瞬间压力过大,宕机。
解决方式:
将失效时间分散开
通过使用自动生成随机数使得key的过期时间是随机的,防止集体过期
使用多级架构
使用nginx缓存+redis缓存+其他缓存,不同层使用不同的缓存,可靠性更强
设置缓存标记
记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去跟新实际的key
使用锁或者队列的方式
如果查不到就加上排它锁,其他请求只能进行等待
3.缓存击穿
产生缓存雪崩的原因:redis中的某个热点key过期,但是此时有大量的用户访问该过期key, 高并发时,当一个key非常热点(类似于爆款),在不停的扛着大并发,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库并设置到缓存中,导致性能下降。
解决方案:
提前对热点数据进行设置
类似于新闻、某博等软件都需要对热点数据进行预先设置在redis中
监控数据,适时调整
监控哪些数据是热门数据,实时的调整key的过期时长
使用锁机制
最后的防线,当热点key过期,那么就使用锁机制防止大量的请求直接打在DB
如何保证Redis与数据库的数据一致!
ONE 案例
先删除“缓存”再去更新“数据库”。但是该方案还存在问题:
在高并发情况下,第一个线程删除缓存,还没来得及去操作数据库,这时第二个线程访问缓存,发现为null,于是去数据库查询,获取到需要的值,这时候第一个线程才开始操作数据库,然后设置缓存,但是第二个线程又跟奇怪的将第一个线程刚设置的缓存给覆盖掉,然后就出现“乌龙”,数据不一致的问题也出现了!
解决方案:
① 先操作缓存去修改数据库,但不删除缓存。将这个不删除的缓存设置为一个特殊值(*123),当客户端读缓存的时候,发现有前缀包含( * ???),知道他是坏值,就会进行休眠(1秒这样),然后再去查询Redis。 //这样做的弊端是:特殊值对业务可能出现影响,休眠时间会重复(高并发情况下,修改操作频繁,反复会修改这个特殊值的内容,然后同时出现睡眠),影响性能。
②延迟双删,先删除缓存数据,再把数据更新到数据库中,休眠一会(根据业务逻辑的耗时,更改休眠时间)后再次删除该缓存数据。若线程1是更新请求,线程2是查询请求,延迟双删,可以保证再这两个请求同时存在的情况下的数据一致性!确保查询请求结束,更新请求可以删除查询请求造成的缓存脏数据。
总结:写操作不能太频繁!
TWO 案例
先删除“数据库”再去更新“缓存”。
该案例的问题是:数据库写完之后,再删除缓存,但删除失败了,这会导致数据不一致。
解决方案:
①给缓存设置一个过期时间,但缺点是,过期时间内 不能保证数据是有用的数据,可能是上次没删掉的坏数据。
②引入MQ,保证原子操作。
一个去删缓存,一个去操作数据库。MQ若是删除操作失败了,启动MQ重试机制,在重试的这段时间,缓存数据不会更新。
③将热点数据缓存设置为永不过期,但是在value当中写入一个逻辑上的过期时间,另外起一个后台线程,扫描这些key,对于已逻辑上过期的缓存,进行删除。
总结:始终只能保证一定时间内的最终一致性。
THREE 案例
Redis和Mysql集群实现的读写分离架构
如果MySQL采用的是读写分离的架构,主从服务器之间也会存在时间差,也就是A更新操作,删除缓存,并请求主数据库进行数据更新,主库与从库进行同步数据的操作,B进行查询操作时,缓存中没有数据,就去从库中读取数据,此时主从数据未更新完成,拿到的还是旧数据。
解决方法是:查询数据经过Redis时,若查询缓存为空时,强制将其指向主数据库中进行查询。
Four 案例
异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
读Redis:热数据基本都在Redis
写MySQL:增删改都是操作MySQL
更新Redis数据:MySQ的数据操作binlog,来更新到Redis Redis更新
①数据操作主要分为两大块:
一个是全量(将全部数据一次写入到redis)
一个是增量(实时更新) 这里说的是增量,指的是mysql的update、insert、delate变更数据。
②读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
参考:redis常见面试题_鲨鱼辣椒_TUT的博客-CSDN博客
反射
1.反射是什么?
反射(Reflection)能够让运行于 JVM 中的程序检测和修改运行时的行为。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.
反射就是把java类中的各种成分映射成一个个的Java对象
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
2.反射机制有什么用?
运行时动态获取类的信息:在编写代码时,对于类的信息是必须在编译时确定的,但在运行时,有时需要根据某些条件,动态获取某个类的信息,这时就可以使用Java中的反射机制。
动态生成对象:反射机制可以在运行时生成对象,这样就可以根据参数的不同,动态的创建不同的类的实例对象。
动态调用方法:通过反射机制可以调用类中的方法,不论这些方法是否是公共的,也不论这些方法的参数个数和类型是什么,反射机制都具有这样的能力。
动态修改属性:利用反射机制可以获取到类中的所有成员变量,并可以对其进行修改。
实现动态代理:利用反射机制可以实现代理模式,通过代理对象完成原对象对某些方法的调用,同时也可以在这些方法的调用前后做一些额外的处理。
3.反射的实现原理?
调用反射的总体流程如下:
1、当我们编写完一个Java项目之后,每个java文件都会被编译成一个.class文件。
2、这些class文件在程序运行时会被ClassLoader加载到JVM中,当一个类被加载以后,JVM就会在内存中自动产生一个Class对象。
3、通过Class对象获取Field/Method/Construcor
我们一般平时是通过new的形式创建对象实际上就是通过这些Class来创建的,只不过这个class文件是编译的时候就生成的,程序相当于写死了给jvm去跑。
反射是什么呢?当我们的程序在运行时,需要动态的加载一些类这些类可能之前用不到所以不用加载到jvm,而是在运行时根据需要才加载。
原来使用new的时候,需要明确的指定类名,这个时候属于硬编码实现,而在使用反射的时候,可以只传入类名参数,就可以生成对象,降低了耦合性,使得程序更具灵活性。
4.java反射技术的应用场景
框架开发:许多流行的Java框架,比如Spring、Hibernate、Struts等,都使用了反射机制,以提供更灵活、可扩展的特性。
应用程序开发:反射机制常常用于某些需要动态加载或访问类信息的应用程序中,比如动态配置,插件管理等。
单元测试:JUnit测试框架中,反射机制被广泛应用,可以方便地创建测试对象和调用测试方法。
动态代理:反射机制可以实现动态代理,实现不改变原来代码的情况下,对原来对象的方法进行增强。
JavaBean工具:JavaBean工具中,使用反射机制可以获取类的属性名、属性值、调用属性的setter和getter方法等信息,方便进行对象的序列化与反序列化操作。
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架,也是利用CGLIB 反射机制才得以实现,下面就举例最常见的两个例子,来说明反射机制的强大之处:
- JDBC 的数据库的连接
在JDBC 的操作中,如果要想进行数据库的连接,则必须按照以上的几步完成
- 通过Class.forName()加载数据库的驱动程序 (通过反射加载,前提是引入相关了Jar包)
- 通过 DriverManager 类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码
- 通过Connection 接口接收连接
public class ConnectionJDBC {
/**
* @param args
*/
//驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中
public static final String DBDRIVER = "com.mysql.jdbc.Driver";
//连接地址是由各个数据库生产商单独提供的,所以需要单独记住
public static final String DBURL = "jdbc:mysql://localhost:3306/test";
//连接数据库的用户名
public static final String DBUSER = "root";
//连接数据库的密码
public static final String DBPASS = "";
public static void main(String[] args) throws Exception {
Connection con = null; //表示数据库的连接对象
Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现
con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库
System.out.println(con);
con.close(); // 3、关闭数据库
}
- Spring 框架的使用
在 Java的反射机制在做基础框架的时候非常有用,行内有一句这样的老话:反射机制是Java框架的基石。一般应用层面很少用,不过这种东西,现在很多开源框架基本都已经封装好了,自己基本用不着写。典型的除了hibernate之外,还有spring也用到很多反射机制。最经典的就是xml的配置模式。
Spring 通过 XML 配置模式装载 Bean 的过程:
- 将程序内所有 XML 或 Properties 配置文件加载入内存中
- Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制,根据这个字符串获得某个类的Class实例
- 动态配置实例的属性
Spring这样做的好处是:
- 不用每一次都要在代码里面去new或者做其他的事情
- 以后要改的话直接改配置文件,代码维护起来就很方便了
- 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现
模拟 Spring 加载 XML 配置文件:
public class BeanFactory {
private Map<String, Object> beanMap = new HashMap<String, Object>();
/**
* bean工厂的初始化.
* @param xml xml配置文件
*/
public void init(String xml) {
try {
//读取指定的配置文件
SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//从class目录下获取指定的xml文件
InputStream ins = classLoader.getResourceAsStream(xml);
Document doc = reader.read(ins);
Element root = doc.getRootElement();
Element foo;
//遍历bean
for (Iterator i = root.elementIterator("bean"); i.hasNext();) {
foo = (Element) i.next();
//获取bean的属性id和class
Attribute id = foo.attribute("id");
Attribute cls = foo.attribute("class");
//利用Java反射机制,通过class的名称获取Class对象
Class bean = Class.forName(cls.getText());
//获取对应class的信息
java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(bean);
//获取其属性描述
java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors();
//设置值的方法
Method mSet = null;
//创建一个对象
Object obj = bean.newInstance();
//遍历该bean的property属性
for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {
Element foo2 = (Element) ite.next();
//获取该property的name属性
Attribute name = foo2.attribute("name");
String value = null;
//获取该property的子元素value的值
for(Iterator ite1 = foo2.elementIterator("value"); ite1.hasNext();) {
Element node = (Element) ite1.next();
value = node.getText();
break;
}
for (int k = 0; k < pd.length; k++) {
if (pd[k].getName().equalsIgnoreCase(name.getText())) {
mSet = pd[k].getWriteMethod();
//利用Java的反射极致调用对象的某个set方法,并将值设置进去
mSet.invoke(obj, value);
}
}
}
//将对象放入beanMap中,其中key为id值,value为对象
beanMap.put(id.getText(), obj);
}
} catch (Exception e) {
System.out.println(e.toString());
}
}
//other codes
}