一、对JVM的了解
1.1 什么是JVM?
JVM(Java Virtual Machine),俗称Java虚拟机。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。
1.2 内部结构
JVM的内部体系结构分为三部分
- 类装载器(ClassLoader)子系统
- 运行时数据区
- 执行引擎
1.3 执行引擎
它或者在执行字节码,或者执行本地方法。
主要的执行技术有:解释,即时编译,自适应优化、芯片级直接执行其中解释属于第一代JVM,即时编译JIT属于第二代JVM,自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式 。
自适应优化:开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
1.4 运行时数据区
主要包括:
(1) 方法区:当JVM的类装载器加载.class文件,并进行解析,把解析的类型信息放入方法区。
(2)堆:虚拟机中只有一个堆,所有的线程都共享他。存放所有程序在运行时创建的对象。
(3)Java栈:存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
(4) PC寄存器:每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
(5)本地方法栈:用于支持native方法的执行,存储了每个native方法调用的状态。对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑。
1.5 JVM垃圾回收(GC)
Sun的JVMGenerationalCollecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)
通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的MethodArea,不属于Heap。
GC的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停
(1)对新生代的对象的收集称为minor GC;
(2)对旧生代的对象的收集称为major GC;
(3)程序中主动调用System.gc()强制执行的GC为Full GC。
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收);
(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC);
(3)弱引用:在GC时一定会被GC回收;
(4)虚引用:由于虚引用只是用来得知对象是否被GC。
Young(年轻代)
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
Perm(持久代)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
二、对反射的了解
2.1 什么是反射?
在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
2.2 为什么要使用反射?反射有什么作用?
(1)在运行时判断任意一个对象所属的类;
(2)在运行时构造任意一个类的对象;
(3)在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
(4)在运行时调用任意一个对象的方法。
2.3 反射使用
a. 通过一个全限类名创建一个对象
(1)Class.forName("全限类名"); 例如:com.mysql.jdbc.Driver Driver类已经被加载到 jvm中,并且完成了类的初始化工作就行了
(2)类名.class; 获取Class<?> clz 对象
(3)对象.getClass();
b. 获取构造器对象,通过构造器new出一个对象
(1)Clazz.getConstructor([String.class]);
(2)Con.newInstance([参数]);
c. 通过class对象创建一个实例对象(就相当与new类名()无参构造器)
(1)Clazz.newInstance();
d. 通过class对象获得一个属性对象
(1)Field c=clz.getFields():获得某个类的所有的公共(public)的字段,包括父类中的字段。
(2)Field c=clz.getDeclaredFields():获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段
e. 通过class对象获得一个方法对象
(1)Clazz.getMethod("方法名",class…..parameaType);(只能获取公共的)
(2)Clazz.getDeclareMethod("方法名");(获取任意修饰的方法,不能执行私有)
(3)M.setAccessible(true);(让私有的方法可以执行)
f. 让方法执行
(1)Method.invoke(obj实例对象,obj可变参数);-----(是有返回值的)
2.4 反射的优缺点?
- 优点:
- 增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作;
- 在运行时可以获取任意一个类的方法、属性,并且还能通过反射进行动态调用。
- 缺点:
- 反射会涉及到动态类型的解析,导致性能要比非反射调用更低;
- 使用反射以后,代码的可读性会下降;
- 反射可以绕过一些限制访问的属性或者方法,可能会导致破坏了代码本身的抽象性。
2.5 哪里会用到反射机制
(1)我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
(2)Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:
①将程序内所有 XML 或 Properties 配置文件加载入内存中;
②Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
③使用反射机制,根据这个字符串获得某个类的Class实例;
④动态配置实例的属性。
2.6 什么是 java 序列化?什么情况下需要序列化?
序列化:将 Java 对象转换成字节流的过程。
反序列化:将字节流转换成 Java 对象的过程。
当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。
序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。
注意事项:
某个类可以被序列化,则其子类也可以被序列化;
声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据;
反序列化读取序列化对象的顺序要保持一致。
2.7 反射与序列化和反序列化的关系
序列化:
序列化需要将一个对象信息转化为某种可以保存和传输的格式,而要将类对象序列化为某种格式有两种方式:
1.熟悉类的结构,例如知道类中的属性和方法,可以直接序列化相应的类属性信息,将类对象保存为一个格式。
2.不熟悉类的结构,在调用实例化对象时候,需要将类对象信息封装为某个格式进行传输,这时就需要动态的解析出该类中的属性和方法了,这就可以通过【反射】来实现,反射动态的获取到类的属性和方法,从而对其进行序列化的操作。
反序列化:
与序列化关系相反,一个类对象如果想要反序列化,首要的就是先【反射】出原始类的属性信息,通过传输过来的格式化信息,对格式化信息进行解析并进行属性赋值。
当然反射机制在java中使用更多,JVM中常常通过反射机制进行动态的修改和检查,还有一些注解模式,往往通过反射机制,将其相应的操作进行实现。
2.8 动态代理是什么?有哪些应用?
动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。
Java 中实现动态的方式:JDK 中的动态代理 和 Java类库 CGLib。
应用场景:
统计每个 api 的请求耗时;
统一的日志输出;
校验被调用的 api 是否已经登录和权限鉴定;
Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程。
2.9 怎么实现动态代理?
使用的模式:代理模式。
代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。类似租房的中介。
两种动态代理:
(1)jdk动态代理:jdk动态代理是由Java内部的反射机制来实现的,目标类基于统一的接口(InvocationHandler);
(2)cglib动态代理:cglib动态代理底层则是借助asm来实现的,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。
2.10 Java反射创建对象效率高还是通过new创建对象的效率高
通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低。
三、几种数据结构的查找、插入、删除的时间复杂度对比
数据结构 | 查找 | 插入 | 删除 |
数组 | O(n) | O(n) | O(n) |
有序数组 | O(logn)(二分查找) | O(n) | O(n) |
单链表 | O(n) | O(n) | O(n) |
有序单链表 | O(n) | O(n) | O(n) |
双链表 | O(n) | O(n) | O(n) |
有序双链表 | O(n) | O(n) | O(n) |
二叉树 | O(n) | O(n) | O(n) |
二叉搜索树 | O(logn) | O(logn) | O(logn) |
红黑树 | O(logn) | O(logn) | O(logn) |
平衡二叉树 | O(logn) | O(logn) | O(logn) |
哈希表 | O(1) | O(1) | O(1) |
四、Mysql:左查询的特点,左右查询的区别
MySQL中的左查询(LEFT JOIN)是一种连接查询,它根据左边的表的所有行来匹配右边的表,并返回匹配的结果。左查询的特点如下:
1. 返回左表的全部行:左查询会返回左边表的全部行,即使右边表中没有匹配的行。
2. 匹配的行为NULL:如果左表的某一行在右表中没有匹配的行,那么对应的右表的列会被设置为NULL。
3. 保留左表的重复行:如果左表中有重复的行,那么左查询会返回所有匹配的结果,包括重复的行。
左查询的语法如下:
SELECT 列表
FROM 左表
LEFT JOIN 右表 ON 连接条件
左右查询(LEFT JOIN和RIGHT JOIN)的区别在于连接的方向。左查询是以左表为基准,返回左表的全部行,而右查询是以右表为基准,返回右表的全部行。其他特点和用法与左查询相似。需要注意的是,左查询和右查询在结果上可能是相同的,只是返回的结果集中左右表的位置不同。在实际使用中,选择左查询还是右查询取决于具体的需求和数据表的结构。
五、Redis的应用场景
5.1 缓存
- DB缓存,减轻DB服务器压力;
- 提高系统响应。
作为Key-Value形态的内存数据库,Redis 最先会被想到的应用场景便是作为数据缓存。而使用 Redis 缓存数据非常简单,只需要通过string类型将序列化后的对象存起来即可,不过也有一些需要注意的地方:
- 必须保证不同对象的 key 不会重复,并且使 key 尽量短,一般使用类名(表名)加主键拼接而成。
- 选择一个优秀的序列化方式也很重要,目的是提高序列化的效率和减少内存占用。
- 缓存内容与数据库的一致性,这里一般有两种做法:
1.只在数据库查询后将对象放入缓存,如果对象发生了修改或删除操作,直接清除对应缓存(或设为过期);
2.在数据库新增和查询后将对象放入缓存,修改后更新缓存,删除后清除对应缓存(或设为过期)。
5.2 数据共享分布式
String 类型,因为 Redis 是分布式的独立服务,可以在多个应用之间共享。
例如:分布式Session
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
5.3 分布式锁
如今都是分布式的环境下java自带的单体锁已经不适用的。在 Redis 2.6.12 版本开始,string的set命令增加了一些参数:
- EX:设置键的过期时间(单位为秒)。
- PX:设置键的过期时间(单位为毫秒)。
- NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
- XX :只在键已经存在时,才对键进行设置操作。
由于这个操作是原子性的,可以简单地以此实现一个分布式的锁,例如:
set lock_key locked NX EX 1
如果这个操作返回false,说明 key 的添加不成功,也就是当前有人在占用这把锁。而如果返回true,则说明得了锁,便可以继续进行操作,并且在操作后通过del命令释放掉锁。并且即使程序因为某些原因并没有释放锁,由于设置了过期时间,该锁也会在 1 秒后自动释放,不会影响到其他程序的运行。
推荐使用 redisson 第三方库实现分布式锁。
5.4 全局ID
int类型,incrby,利用原子性。
incrby userid 1000
分库分表的场景,一次性拿一段。
5.5 计数器
int类型,incr方法。
例如:文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库
计数功能应该是最适合 Redis 的使用场景之一了,因为它高频率读写的特征可以完全发挥 Redis 作为内存数据库的高效。在 Redis 的数据结构中,string、hash和sorted set都提供了incr方法用于原子性的自增操作,下面举例说明一下它们各自的使用场景:
- 如果应用需要显示每天的注册用户数,便可以使用string作为计数器,设定一个名为REGISTERED_COUNT_TODAY的 key,并在初始化时给它设置一个到凌晨 0 点的过期时间,每当用户注册成功后便使用incr命令使该 key 增长 1,同时当每天凌晨 0 点后,这个计数器都会因为 key 过期使值清零。
- 每条微博都有点赞数、评论数、转发数和浏览数四条属性,这时用hash进行计数会更好,将该计数器的 key 设为weibo:weibo_id,hash的 field 为like_number、comment_number、forward_number和view_number,在对应操作后通过hincrby使hash 中的 field 自增。
- 如果应用有一个发帖排行榜的功能,便选择sorted set吧,将集合的 key 设为POST_RANK。当用户发帖后,使用zincrby将该用户 id 的 score 增长 1。sorted set会重新进行排序,用户所在排行榜的位置也就会得到实时的更新。
5.6 限流
int类型,incr方法。以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false。
5.7 位统计
String类型的bitcount。
字符是以8位二进制存储的
set k1 a
setbit k1 6 1
setbit k1 7 0
get k1
/* 6 7 代表的a的二进制位的修改
a 对应的ASCII码是97,转换为二进制数据是01100001
b 对应的ASCII码是98,转换为二进制数据是01100010
因为bit非常节省空间(1 MB=8388608 bit),可以用来做大数据量的统计。
*/
5.8 时间轴(Timeline)
list作为双向链表,不光可以作为队列使用。如果将它用作栈便可以成为一个公用的时间轴。当用户发完微博后,都通过lpush将它存放在一个 key 为LATEST_WEIBO的list中,之后便可以通过lrange取出当前最新的微博。
5.9 消息队列
Redis 中list的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列(生产者 / 消费者模型)。消息的生产者只需要通过lpush将消息放入 list,消费者便可以通过rpop取出该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择sorted set。而pub/sub功能也可以用作发布者 / 订阅者模型的消息。无论使用何种方式,由于 Redis 拥有持久化功能,也不需要担心由于服务器故障导致消息丢失的情况。
List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间:
- blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
- brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
上面的操作。其实就是java的阻塞队列。学习的东西越多。学习成本越低
- 队列:先进先除:rpush blpop,左头右尾,右边进入队列,左边出队列。
- 栈:先进后出:rpush brpop。
5.10 抽奖
利用set结构的无序性,通过 Spop( Redis Spop 命令用于移除集合中的指定 key 的一个或多个随机元素,移除后会返回移除的元素。) 随机获得值
redis> SADD myset "one"
(integer) 1
redis> SADD myset "two"
(integer) 1
redis> SADD myset "three"
(integer) 1
redis> SPOP myset
"one"
redis> SMEMBERS myset
1) "three"
2) "two"
redis> SADD myset "four"
(integer) 1
redis> SADD myset "five"
(integer) 1
redis> SPOP myset 3
1) "five"
2) "four"
3) "two"
redis> SMEMBERS myset
1) "three"
redis>
5.11 点赞、签到、打卡
假如上面的微博ID是t1001,用户ID是u3001
用 like:t1001 来维护 t1001 这条微博的所有点赞用户
- 点赞了这条微博:sadd like:t1001 u3001;
- 取消点赞:srem like:t1001 u3001;
- 是否点赞:sismember like:t1001 u3001;
- 点赞的所有用户:smembers like:t1001;
- 点赞数:scard like:t1001。
是不是比数据库简单多了。
5.12 商品标签
老规矩,用 tags:i5001 来维护商品所有的标签。
- sadd tags:i5001 画面清晰细腻;
- sadd tags:i5001 真彩清晰显示屏;
- sadd tags:i5001 流程至极。
5.13 好友关系、用户关注、推荐模型
这个场景最开始是是一篇介绍微博 Redis 应用的 PPT 中看到的,其中提到微博的 Redis 主要是用在在计数和好友关系两方面上,当时对好友关系方面的用法不太了解,后来看到《Redis 设计与实现》中介绍到作者最开始去使用 Redis 便是希望能通过set解决传统数据库无法快速计算集合中交集这个功能。后来联想到微博当前的业务场景,确实能够以这种方式实现,所以姑且猜测一下:
对于一个用户 A,将它的关注和粉丝的用户 id 都存放在两个 set 中:
- A:follow:存放 A 所有关注的用户 id;
- A:follower:存放 A 所有粉丝的用户 id。
那么通过sinter命令便可以根据A:follow和A:follower的交集得到与 A 互相关注的用户。当 A 进入另一个用户 B 的主页后,A:follow和B:follow的交集便是 A 和 B 的共同专注,A:follow和B:follower的交集便是 A 关注的人也关注了 B。
举例
follow 关注 fans 粉丝
相互关注:
- sadd 1:follow 2
- sadd 2:fans 1
- sadd 1:fans 2
- sadd 2:follow 1
我关注的人也关注了他(取交集):
- sinter 1:follow 2:fans
可能认识的人:
- 用户1可能认识的人(差集):sdiff 2:follow 1:follow
- 用户2可能认识的人:sdiff 1:follow 2:follow
5.14 排行榜
使用sorted set(有序set)和一个计算热度的算法便可以轻松打造一个热度排行,zrevrangebyscore可以得到以分数倒序排列的序列,zrank可以得到一个成员在该排行榜的位置(是分数正序排列时的位置,如果要获取倒序排列时的位置需要用zcard-zrank)。
id 为6001 的新闻点击数加1:
zincrby hotNews:20190926 1 n6001
获取今天点击最多的15条:
zrevrange hotNews:20190926 0 15 withscores
5.15 倒排索引
倒排索引是构造搜索功能的最常见方式,在 Redis 中也可以通过set进行建立倒排索引,这里以简单的拼音 + 前缀搜索城市功能举例:
假设一个城市北京,通过拼音词库将北京转为beijing,再通过前缀分词将这两个词分为若干个前缀索引,有:北、北京、b、be…beijin和beijing。将这些索引分别作为set的 key(例如:index:北)并存储北京的 id,倒排索引便建立好了。接下来只需要在搜索时通过关键词取出对应的set并得到其中的 id 即可。
5.16 显示最新的项目列表
比如说,我们的一个Web应用想要列出用户贴出的最新20条评论。在最新的评论边上我们有一个“显示全部”的链接,点击后就可以获得更多的评论。
每次新评论发表时,我们会将它的ID添加到一个Redis列表。可以限定列表的长度为5000
LPUSH latest.comments
在Redis中我们的最新ID使用了常驻缓存,这是一直更新的。但是我们做了限制不能超过5000个ID,因此我们的获取ID函数会一直询问Redis。只有在超出了这个范围的时候,才需要去访问数据库。
六、Redis用的最多的基本类型,用在哪,可以替代
简述
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
它支持多种类型的数据结构,如 字符串(strings),散列(hashes), 列表(lists),集合(sets),有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
6.1 String
String字符串
String是redis最基本的类型,一个key对应一个value。
- String类型是二进制安全的,意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。
- String类型是redis最基本的数据类型,一个redis中字符串value最多可以是512M
命令
基本操作
- set (key) (value):设置键值对;
- setnx (key) (value):防止覆盖,设置键值对;
- 如果key不存在就设置,返回1
- 如果key已经存在就不设置,返回0
- get(key):获取key对应的value;
- getset (key) (value) :先get再set,返回旧值,如果没有旧值返回nil;
- append (key) (value):向指定的key的value后追加字符串;
- del (key) :删除key;
- strlen (key):获取key对应值的字符串长度。
数字value的加减
- incr (key) :value + 1;
- decr (key) :value - 1;
- incrby (key) (number):value + number;
- decrby (key) (number):value - number。
获取或者设置指定范围内的值
- getrange (key) (begin) (end) :获取[begin,end]下标范围内的值,如果是(0,1)就是获取所有值
- setrange (key) (begin) (xxxx) :从begin下标开始设置xxx值,将原有的替换掉
设置键值过期时间
- setex (key) (seconds) expire:设置键过期时间
- ttl (key) :查看key剩余存活时间
同时设置或获取多个key-value
- met (key1) (value1) (key2) (value2):用于同时设置一个或多个 key-value 对
- mget (key1) (key2) :返回所有(一个或多个)给定 key 的值(如果某个key不存在,不存在的key返回null)
- msetnx(key1) (value1) (key2) (value2):当所有 key 都成功设置,返回 1 。 如果有一个key设置失败,所有的key设置都会失败,返回 0 。原子操作
实战场景
- 缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
- 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
- session:常见方案spring session + redis实现session共享。
编码与底层结构
字符串是Redis最基本的数据类型,不仅所有key都是字符串类型,其它几种数据类型构成的元素也是字符串。注意字符串的长度不能超过512M。
编码
字符串对象的编码可以是int,raw或者embstr。
1.int 编码:保存的是可以用 long 类型表示的整数值。
2.embstr 编码:保存长度小于44字节的短字符串(redis3.2版本之前是39字节,之后是44字节)。
- 对其进行修改后变成raw编码,无论是否达到44字节
3.raw 编码:保存长度大于44字节的长字符串(redis3.2版本之前是39字节,之后是44字节)。
内存布局
字符串对象支持三种编码方式: RAW, INT, EMBSTR。
raw 和 embstr 的区别:
其实 embstr 编码是专门用来保存短字符串的一种优化编码,embstr与raw都使用redisObject和sds保存数据,
- embstr的使用只分配一次内存空间(因此redisObject和sds是连续的)
- raw需要分配两次内存空间(分别为redisObject和sds分配空间)
只分配一次内存空间的好处是,创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。
而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
编码的转换
- 当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。
- 对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。
6.2 List
Redis中的List其实就是双端链表
- 使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。
- Redis还提供了操作List中某一段的api,你可以直接查询,删 除List中某一段的元素。
- 使用列表的技巧
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpush+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
命令使用
命令 | 简述 | 使用 |
RPUSH | 将给定值推入到列表右端 | RPUSH key value |
LPUSH | 将给定值推入到列表左端 | LPUSH key value |
RPOP | 从列表的右端弹出一个值,并返回被弹出的值 | RPOP key |
LPOP | 从列表的左端弹出一个值,并返回被弹出的值 | LPOP key |
LRANGE | 获取列表在给定范围上的所有值 | LRANGE key 0 -1 |
LINDEX | 通过索引获取列表中的元素。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 | LINEX key index |
使用场景
- 微博TimeLine: 有人发布微博,用lpush加入时间轴,展示新的列表信息。
- 消息队列
编码与底层结构
list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。
列表对象的编码是quicklist。 (之前版本中有linked和ziplist这两种编码)
6.3 Set
简介
Redis 的 Set 是 String 类型的无序集合。
- 集合成员是不可重复的
- Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)
使用场景
- 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
- 点赞,或点踩,收藏等,可以放到set中实现
命令使用
命令 | 简述 | 使用 |
SADD | 向集合添加一个或多个成员 | SADD key value |
SCARD | 获取集合的成员数 | SCARD key |
SMEMBER | 返回集合中的所有成员 | SMEMBER key member |
SISMEMBER | 判断 member 元素是否是集合 key 的成员 | SISMEMBER key member |
编码与底层结构
集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。
注意集合和列表的区别:
- 集合中的元素是无序的,因此不能通过索引来操作元素;
- 集合中的元素不能重复
编码
集合对象的编码可以是 intset 或者 hashtable;
底层结构
对应的底层实现分别是intset和dict
- 显然当使用intset作为底层实现的数据结构时, 集合中存储的只能是数值数据, 且必须是整数;
- 当使用dict作为集合对象的底层实现时, 是将数据全部存储于dict的键中, 值字段闲置不用.
编码转换
当集合同时满足以下两个条件时,使用 intset
编码,否则使用 hashtable
编码
1、集合对象中所有元素都是整数;
2、集合对象所有元素数量不超过512(可以通过配置文件的 set-max-intset-entries
进行配置)。
6.4 Hash散列
简介
Redis hash 是一个 string 类型的 field(字段) 和 value(属性) 的映射表,hash 特别适合用于存储对象。
一个hash可以存多个key-value,类似一个对象的多个字段和属性。
使用场景:
- 缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
命令使用
命令 | 简述 | 使用 |
HSET | 添加键值对 | HSET hash-key sub-key1 value1 |
HGET | 获取指定散列键的值 | HGET hash-key key1 |
HGETALL | 获取散列中包含的所有键值对 | HGETALL hash-key |
HDEL | 如果给定键存在于散列中,那么就移除这个键 | HDEL hash-key sub-key1 |
编码与底层结构
哈希对象的键是一个字符串类型,值是一个键值对集合。
哈希对象的编码可以是 ziplist
或者 hashtable
;
对应的底层实现有两种, 一种是ziplist
, 一种是dict
。
编码转换
和上面列表对象使用 ziplist 编码一样,当同时满足下面两个条件时,使用ziplist(压缩列表)编码,否则使用hashtable 编码
1、列表保存元素个数小于512个(配置文件中的 set-max-intset-entries 修改)
2、每个元素长度小于64字节
6.5 Zset有序集合
简介
Redis 有序集合和集合一样基本一致。
区别:
- 每个元素都会关联一个 double 类型的权重参数(score),使得集合中的元素能够按score进行有序排列。
特点:
- 有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
使用场景:
- 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
- 成绩排行:比如一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分, 形成了按成绩排序。
- 权重分配:可以用sorted set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
编码与底层结构
有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。
编码
- 一种的编码值宏为ZIPLIST;
- 对应的编码值宏为SKIPLIST。
底层结构
1.一种是使用ziplist作为底层实现,
- 使用ziplist来实现在序集合很容易理解, 只需要在ziplist这个数据结构的基础上做好排序与去重就可以了.
2.另外一种比较特殊, 底层使用了两种数据结构: dict与skiplist.
- 单独使用 哈希表,虽然能以 O(1) 的时间复杂度查找成员,但是字典是以无序的方式来保存集合元素,所以进行范围操作的时候都要进行排序;
- 单独使用跳表,虽然能执行范围操作,但是查找操作为O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。
两者结合起来,既可以以O(1) 的时间复杂度查找成员,又可以实现范围操作
编码转换
当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码,否者使用 skiplist 编码
- 保存的元素数量小于128;(配置文件zset-max-ziplist-entries 修改);
- 保存的所有元素长度都小于64字节。(zset-max-ziplist-value 进行修改)。
七、equals如何判断两个对象相同
在 Java 中,equals 方法是用来判断两个对象是否相等的。通常情况下,如果两个对象的属性值相同,则认为它们相等。但是,在具体实现中,equals 方法需要满足以下几个条件:
- 对称性:如果 a.equals(b) 返回 true,则 b.equals(a) 也应该返回 true。
- 自反性:对于任何非空引用 x,x.equals(x) 都应该返回 true。
- 传递性:如果 a.equals(b) 和 b.equals(c) 都返回 true,则 a.equals(c)也应该返回 true。
- 一致性:如果两个对象的属性值没有发生变化,那么多次调用其 equals 方法应该始终返回相同的结果。
- 非空性:对于任何非空引用 x,x.equals(null) 应该返回 false。
根据这些条件,可以实现不同类型的 equals 方法。例如,对于简单的数值类型和字符串类型,可以使用默认的 equals 方法实现;而对于自定义类,需要根据类的属性来判断是否相等。
八、String str="i"与 String str=new String(“i”)一样吗
不一样
因为内存的分配方式不一样。String str="i"的方式,Java 虚拟机会将其分配到常量池中;而 String str=new String(“i”)方式,则会被分到堆内存中。
解释:
- String str="i" Java 虚拟机会将其分配到常量池中:常量池不会重复创建对象。
在String str1="i"中,把i值存在常量池,地址赋给str1。假设再写一个String str2=“i”,则会把i的地址赋给str2,但是i对象不会重新创建,他们引用的是同一个地址值,共享同一个i内存。
- String str=new String(“i”) 分到堆内存中:堆内存会创建新的对象。
假设再写一个String str3=new String(“i”),则会创建一个新的i对象,然后将新对象的地址值赋给str3。虽然str3和str1的值相同但是地址值不同。
拓展:
堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。
九、接口和抽象类有什么区别
接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。
抽象类是什么:
抽象类不能创建实例,它只能作为父类被继承。抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类的随意性。
(1) 抽象方法只作声明,而不包含实现,可以看成是没有实现体的虚方法;
(2) 抽象类不能被实例化;
(3) 抽象类可以但不是必须有抽象属性和抽象方法,但是一旦有了抽象方法,就一定要把这个类声明为抽象类;
(4) 具体派生类必须覆盖基类的抽象方法;
(5) 抽象派生类可以覆盖基类的抽象方法,也可以不覆盖。如果不覆盖,则其具体派生类必须覆盖它们。
接口是什么:
(1) 接口不能被实例化;
(2) 接口只能包含方法声明;
(3) 接口的成员包括方法、属性、索引器、事件;
(4) 接口中不能包含常量、字段(域)、构造函数、析构函数、静态成员。
接口和抽象类的区别:
(1)抽象类可以有构造方法,接口中不能有构造方法;
(2)抽象类中可以有普通成员变量,接口中没有普通成员变量;
(3)抽象类中可以包含静态方法,接口中不能包含静态方法;
(4) 一个类可以实现多个接口,但只能继承一个抽象类;
(5)接口可以被多重实现,抽象类只能被单一继承;
(6)如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法。
接口和抽象类的相同点:
(1) 都可以被继承;
(2) 都不能被实例化;
(3) 都可以包含方法声明;
(4) 派生类必须实现未实现的方法。