文章目录
- 〇、分布式锁 & 分布式事务
- 一、 Java集合
- 1.2 流程图关系
- 1.3 底层实现
- 1.4 集合与数组的区别
- 1.5 集合的好处
- 1.6 List集合我们以ArrayList集合为例
- 1.7 迭代器的常用方法
- 1.8 ArrayList和Vector的区别
- 1.9 Set集合的特点:
- 1.10 TreeSet对元素进行排序的方式:
- 1.11 List,Set,Map集合的特点 & 区别
- 1.12 HashMap和Hashtable的区别
- 1.3 HashSet 与 HashMap的区别
- 1.4 说一下HashMap的实现原理(非常重要)
- 1.5 HashMap的扩容操作是怎么实现的?
- 1.6 HashMap是怎么解决哈希冲突的?
- 1.7 什么是哈希?
- 1.8 所有散列函数都有如下一个基本特性
- 1.9 什么是哈希冲突?
- 二、 Java基础
- 三、 多线程
- 四、Spring
- spring常用的注解
- Spring的优缺点是什么?
- Spring 框架中都用到了哪些设计模式?
- Spring 应用程序有哪些不同组件?
- 使用 Spring 有以下方式:
- Spring面向切面编程(AOP)
- Spring控制反转(IOC)
- 什么是Spring的依赖注入?
- Spring事务的实现方式和实现原理
- 说一下Spring的事务传播行为
- spring事务的实现原理
- Spring 事务失效的7种场景
- 说一下 spring 的事务隔离?
- Spring框架的事务管理有哪些优点?
- 你更倾向用哪种事务管理类型?
- Spring Beans
- 请解释Spring Bean的生命周期
- 解释Spring支持的几种bean的作用域
- Spring如何处理线程并发问题?
- Spring基于xml注入bean的几种方式:
- Spring的自动装配:
- Spring 框架中都用到了哪些设计模式?
- spring 是如何开启事务的
- spring的事务在什么场景下会失效
- spring 对事务如何进行管理
- Spring JDBC-Spring对事务管理的支持
- spring如何对bean进行自动的事务管理
- Spring事务的实现方式和实现原理:
- 五、springBoot
- springBoot的实现原理
- 什么是 Spring Boot?
- SpringBoot是什么
- Spring Boot的核心功能
- SpringBoot启动过程-流程
- Spring Boot 的核心注解是哪个?
- Spring Boot 自动配置原理是什么?
- 你如何理解 Spring Boot 配置加载顺序?
- spring boot 核心的两个配置文件:
- 什么是 Spring Profiles?
- 如何实现 Spring Boot 应用程序的安全性?
- Spring Boot 中如何解决跨域问题 ?
- Spring Boot 中的监视器是什么?
- 如何在 Spring Boot 中禁用 Actuator 端点安全性?
- 我们如何监视所有 Spring Boot 微服务?
- 什么是 WebSockets?
- 如何重新加载 Spring Boot 上的更改,而无需重新启动服务器?Spring Boot项目如何热部署?
- Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?
- 运行 Spring Boot 有哪几种方式?
- Spring Boot 需要独立的容器运行吗?
- 开启 Spring Boot 特性有哪几种方式?
- 如何使用 Spring Boot 实现异常处理?
- 如何使用 Spring Boot 实现分页和排序?
- 微服务中如何实现 session 共享 ?
- Spring Boot 中如何实现定时任务 ?
- 六、 springMVC
- 七、 Spring Cloud — 注册中心,配置中心,原理详解
- 八、REDIS
- 九、mybatis
〇、分布式锁 & 分布式事务
0-1分布式锁–包含CAP理论模型
参考:文章-分布式锁
概述
随着互联网技术的不断发展,用户量的不断增加,越来越多的业务场景需要用到分布式系统。
分布式系统有一个著名的理论CAP,指在一个分布式系统中,最多只能同时满足下面三项中的两项:
一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)
可用性(Availability):保证每个请求不管成功或者失败都有响应
分区容错性(Partition tolerance):系统中任意信息的丢失或失败不会影响系统的继续运作
所以在设计系统时,往往需要权衡,在CAP中作选择,要么AP,要么CP、要么AC。
当然,这个理论也并不一定完美,不同系统对CAP的要求级别不一样,选择需要考虑方方面面。
而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,这个时候就需要使用分布式锁。
分布式锁:
当在分布式模型下,数据只有一份(或有限制),此时需要利用锁技术来控制某一时刻修改数据的进程数。这种锁即为分布式锁。
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁应该具备哪些条件:
互斥性:在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
高可用的获取锁与释放锁;
高性能的获取锁与释放锁;
可重入性:具备可重入特性,具备锁失效机制,防止死锁,即就算一个客户端持有锁的期间崩溃而没有主动释放锁,也需要保证后续其他客户端能够加锁成功
非阻塞:具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
分布式锁的业务场景:
互联网秒杀(商品库存)
抢优惠券
分布式锁的实现方式有:
主要有几种实现方式:
基于数据库实现
基于Zookeeper实现
基于Redis实现
如图:
分布式锁对比
从理解的难易程度角度(从低到高):数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高):Zookeeper >= 缓存 > 数据库
从性能角度(从高到低):缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低):Zookeeper > 缓存 > 数据库
基于Zookeeper - 分布式锁
实现思想
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。
基于ZooKeeper实现分布式锁的步骤如下:
创建一个目录mylock;
线程A想获取锁就在mylock目录下创建临时顺序节点;
获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
整个过程如图:
业界推荐直接使用Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
使用方式很简单:
InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock");
interProcessMutex.acquire();
interProcessMutex.release();
其他分布式锁的核心源码如下:
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception{
boolean haveTheLock = false;
boolean doDelete = false;
try {
if ( revocable.get() != null ) {
client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}
while ((client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {
// 获取当前所有节点排序后的集合
List<String> children = getSortedChildren();
// 获取当前节点的名称
String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
// 判断当前节点是否是最小的节点
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
if ( predicateResults.getsTheLock() ) {
// 获取到锁
haveTheLock = true;
} else {
// 没获取到锁,对当前节点的上一个节点注册一个监听器
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this){
Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
if ( stat != null ){
if ( millisToWait != null ){
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 ){
doDelete = true; // timed out - delete our node
break;
}
wait(millisToWait);
}else{
wait();
}
}
}
// else it may have been deleted (i.e. lock released). Try to acquire again
}
}
}
catch ( Exception e ) {
doDelete = true;
throw e;
} finally{
if ( doDelete ){
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
其实 Curator 实现分布式锁的底层原理和上面分析的是差不多的。如图详细描述其原理:
另外,可基于Zookeeper自身的特性和原生Zookeeper API自行实现分布式锁。
优缺点
优点:
可靠性非常高
性能较好
CAP模型属于CP,基于ZAB一致性算法实现
缺点:
性能并不如Redis(主要原因是在写操作,即获取锁释放锁都需要在Leader上执行,然后同步到follower)
实现复杂度高
基于Redis - 分布式锁
实现思想
主要是基于命令:SETNX key value
命令官方文档:https://redis.io/commands/setnx
用法可参考:Redis命令参考
如图:
实现思想的具体步骤:
获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
优缺点
优点:
- 性能非常高
- 可靠性较高
- CAP模型属于AP
缺点:
- 复杂度较高
- 无一致性算法,可靠性并不如Zookeeper
- 锁删除失败 过期时间不好控制
- 非阻塞,获取失败后,需要轮询不断尝试获取锁,比较消耗性能,占用cpu资源
Redis分布式锁实现-例子
以减库存接口为例子,访问接口的时候自动减商品的库存
方案(一)
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
//获取redis中的库存
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
return "success";
}
}
表示:
- 先从Redis中读取stock的值,表示商品的库存
- 判断商品库存是否大于0,如果大于0,则库存减1,然后再保存到Redis里面去,否则就报错
改进 方案(一) :
这种简单的从Redis读取、判断值再减1保存到Redis的操作,很容易在并发场景下出问题:
- 商品超卖
比如:
假设商品的库存有50个,有3个用户同时访问该接口,先是同时读取Redis中商品的库存值,即都是读取到了50,即同时执行到了这一行:
int stock = Integer.valueOf(valueOperations.get("stock"));
然后减1,即到了这一行:
int newStock = stock - 1;
此时3个用户的realStock都是49,然后3个用户都去设置stock为49,那么就会产生库存明明被3个用户抢了,理论上是应该减去3的,结果库存数只减去了1导致商品超卖。
这种问题的产生原因是因为读取库存、减库存、保存到Redis这几步并不是原子操作
那么可以使用加并发锁synchronized来解决:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
synchronized (this) {
//获取redis中的库存
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
}
return "success";
}
}
注意:在Java中关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块。
再改进 方案(一)
以上的代码在单体模式下并没太大问题,但是在分布式或集群架构环境下存在问题,比如架构如下:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/a6ee54660f47dce4e5d8cda380080bbd.png)
在分布式或集群架构下,synchronized只能保证当前的主机在同一时刻只能有一个线程执行减库存操作,但如图同时有多个请求过来访问的时候,不同主机在同一时刻依然是可以访问减库存接口的,这就导致问题1(商品超卖)在集群架构下依然存在。
解决方法
使用如下方案(二)的分布式锁进行解决; 注意:方案(一)并不是分布式锁
方案(二)
分布式锁的简单实现图如下:
简单的实现代码如下:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String lockKey = "product_001";
//加锁: setnx
Boolean isSuccess = valueOperations.setIfAbsent(lockKey, "1");
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
//释放锁
redisTemplate.delete(lockKey);
return "success";
}
}
其实就是对每一个商品加一把锁,代码里面是product_001
- 使用setnx对商品进行加锁
- 如成功说明加锁成功,如失败说明有其他请求抢占了该商品的锁,则当前请求失败退出
- 加锁成功之后进行扣减库存操作
- 删除商品锁
以上的代码方式是有可能会造成死锁的,比如说加锁成功之后,扣减库存的逻辑可能抛异常了,即并不会执行到释放锁的逻辑,那么该商品锁是一直没有释放,会成为死锁的,其他请求完全无法扣减该商品的
使用try...catch...finally
的方式可以解决抛异常的问题,如下:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String lockKey = "product_001";
try {
//加锁: setnx
Boolean isSuccess = valueOperations.setIfAbsent(lockKey, "1");
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
} finally {
//释放锁
redisTemplate.delete(lockKey);
}
return "success";
}
}
改进方案(二)
那么上面的方式是不是能够解决死锁的问题呢?
其实不然,除了抛异常之外,比如程序崩溃、服务器宕机、服务器重启、请求超时被终止、发布、人为kill等都有可能导致释放锁的逻辑没有执行,比如对商品加分布式锁成功之后,在扣减库存的时候服务器正在执行重启,会导致没有执行释放锁。
可以通过对锁设置超时时间来防止死锁的发生,使用Redis的expire命令可以对key进行设置超时时间,如图:
代码实现如下:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String lockKey = "product_001";
try {
//加锁: setnx
Boolean isSuccess = valueOperations.setIfAbsent(lockKey, "1");
//expire增加超时时间
redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
} finally {
//释放锁
redisTemplate.delete(lockKey);
}
return "success";
}
}
加锁成功之后,把锁的超时时间设置为10秒,即10秒之后自动会释放锁,避免死锁的发生。
再改进方案(二)
但是上面的方式同样会产生死锁问题,加锁和对锁设置超时时间并不是原子操作,在加锁成功之后,即将执行设置超时时间的时候系统发生崩溃,同样还是会导致死锁。
改进图案如下:
对此,有两种做法:
lua脚本
set原生命令(Redis 2.6.12版本及以上)
一般是推荐使用set命令,Redis官方在2.6.12版本对set命令增加了NX、EX、PX等参数,即可以将上面的加锁和设置时间放到一条命令上执行,通过set命令即可:
命令官方文档:https://redis.io/commands/set
用法可参考:Redis命令参考
如图:
SET key value NX 等同于 SETNX key value命令,并且可以使用EX参数来设置过期时间
注意:其实目前在Redis 2.6.12版本之后,所说的setnx命令,并非单单指Redis的SETNX key value命令,一般是代指Redis中对set命令加上nx参数进行使用,一般不会直接使用SETNX key value命令了
注意:Redis2.6.12之前的版本,只能通过lua脚本来保证原子性了。
如图:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String lockKey = "product_001";
try {
//加锁: setnx 和 expire增加超时时间
Boolean isSuccess = valueOperations.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
} finally {
//释放锁
redisTemplate.delete(lockKey);
}
return "success";
}
}
再再次改进 方案(二)
以上的方式其实还是存在着问题,在高并发场景下会存在问题,超时时间设置不合理导致的问题
大概的流程图可参考:
流程:
- 进程A加锁之后,扣减库存的时间超过设置的超时时间,这里设置的锁是10秒
- 在第10秒的时候由于时间到期了所以进程A设置的锁被Redis释放了(T5)
- 刚好进程B请求进来了,加锁成功(T6)
- 进程A操作完成(扣减库存)之后,把进程B设置的锁给释放了
- 刚好进程C请求进来了,加锁成功
进程B操作完成之后,也把进程C设置的锁给释放了 - 以此类推…
解决方法也很简单:
- 加锁的时候,把值设置为唯一值,比如说UUID这种随机数
- 释放锁的时候,获取锁的值判断value是不是当前进程设置的唯一值,如果是再去删除
如图:
实现的代码如下:
@Service
public class RedisLockDemo {
@Autowired
private StringRedisTemplate redisTemplate;
public String deduceStock() {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String lockKey = "product_001";
String clientId = UUID.randomUUID().toString();
try {
//加锁: setnx 和 expire增加超时时间
Boolean isSuccess = valueOperations.setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if(null == isSuccess || isSuccess) {
System.out.println("服务器繁忙, 请稍后重试");
return "error";
}
//------ 执行业务逻辑 ----start------
int stock = Integer.valueOf(valueOperations.get("stock"));
if (stock > 0) {
int newStock = stock - 1;
//执行业务操作减库存
valueOperations.set("stock", newStock + "");
System.out.println("扣减库存成功, 剩余库存:" + newStock);
} else {
System.out.println("库存已经为0,不能继续扣减");
}
//------ 执行业务逻辑 ----end------
} finally {
if (clientId.equals(valueOperations.get(lockKey))) {
//释放锁
redisTemplate.delete(lockKey);
}
}
return "success";
}
}
分段锁
怎么在高并发的场景去实现一个高性能的分布式锁呢?
电商网站在大促的时候并发量很大:
(1)若抢购不是同一个商品,则可以增加Redis集群的cluster来实现,因为不是同一个商品,所以通过计算 key 的hash会落到不同的 cluster上;
(2)若抢购的是同一个商品,则计算key的hash值会落同一个cluster上,所以加机器也是没有用的。
针对第二个问题,可以使用库存分段锁的方式去实现。
分段锁
假如产品1有200个库存,可以将这200个库存分为10个段存储(每段20个),每段存储到一个cluster上;将key使用hash计算,使这些key最后落在不同的cluster上。
每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存 -> 判断库存是否充足 -> 扣减库存。
具体可以参照 ConcurrentHashMap 的源码去实现,它使用的就是分段锁。
高性能分布式锁具体可参考链接:每秒上千订单场景下的分布式锁高并发优化实践!【石杉的架构笔记】
原理如图:
基于数据库 - 分布式锁
实现思想
主要有两种方式:
悲观锁
乐观锁
A. 悲观锁(排他锁)
利用select * form table where xx=yy for update 排他锁
注意:这里需要注意的是 where xx=yy,xx字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。
**核心思想:**以「悲观的心态」操作资源,无法获得锁成功,就一直阻塞着等待。
注意:该方式有很多缺陷,一般不建议使用。
实现:
创建一张资源锁表:
CREATE TABLE `resource_lock` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的资源名',
`owner` varchar(64) NOT NULL DEFAULT '' COMMENT '锁拥有者',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的资源';
注意:resource_name 锁资源名称必须有唯一索引
使用事务查询更新:
@Transaction
public void lock(String name) {
ResourceLock rlock = exeSql("select * from resource_lock where resource_name = name for update");
if (rlock == null) {
exeSql("insert into resource_lock(reosurce_name,owner,count) values (name, 'ip',0)");
}
}
使用 for update 锁定的资源 :
如果执行成功,会立即返回,执行插入数据库,后续再执行一些其他业务逻辑,直到事务提交,执行结束;
如果执行失败,就会一直阻塞着。
可以在数据库客户端工具上测试出来这个效果,当在一个终端执行了 for update,不提交事务。在另外的终端上执行相同条件的 for update,会一直卡着
虽然也能实现分布式锁的效果,但是会存在性能瓶颈。
优点:
简单易用,好理解,保障数据强一致性。
缺点:
1)在 RR 事务级别,select 的 for update 操作是基于间隙锁(gap lock) 实现的,是一种悲观锁的实现方式,所以存在阻塞问题。
2)高并发情况下,大量请求进来,会导致大部分请求进行排队,影响数据库稳定性,也会耗费服务的CPU等资源。
当获得锁的客户端等待时间过长时,会提示:
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并发情况下,也会造成占用过多的应用线程,导致业务无法正常响应。
3)如果优先获得锁的线程因为某些原因,一直没有释放掉锁,可能会导致死锁的发生。
4)锁的长时间不释放,会一直占用数据库连接,可能会将数据库连接池撑爆,影响其他服务。
5)MySql数据库会做查询优化,即便使用了索引,优化时发现全表扫效率更高,则可能会将行锁升级为表锁,此时可能就更悲剧了。
6)不支持可重入特性,并且超时等待时间是全局的,不能随便改动。
B. 乐观锁
所谓乐观锁与悲观锁最大区别在于基于 CAS思想 ,表中添加一个时间戳或者是版本号的字段来实现,update xx set version=new_version where xx=yy and version=Old_version,通过增加递增的版本号字段实现乐观锁。
不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。
抢购、秒杀就是用了这种实现以防止超卖的现象。
实现:
创建一张资源锁表:
CREATE TABLE `resource` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主键',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '资源名',
`share` varchar(64) NOT NULL DEFAULT '' COMMENT '状态',
`version` int(4) NOT NULL DEFAULT '' COMMENT '版本号',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='资源';
为表添加一个字段,版本号或者时间戳都可以。通过版本号或者时间戳,来保证多线程同时间操作共享资源的有序性和正确性。
伪代码实现:
Resrouce resource = exeSql("select * from resource where resource_name = xxx");
boolean succ = exeSql("update resource set version= 'newVersion' ... where resource_name = xxx and version = 'oldVersion'");
if (!succ) {
// 发起重试
}
实际代码中可以写个while循环不断重试,版本号不一致,更新失败,重新获取新的版本号,直到更新成功。
优缺点
优点:
实现简单,复杂度低
保障数据一致性
缺点:
性能低,并且有锁表的风险
可靠性差
非阻塞操作失败后,需要轮询,占用CPU资源
长时间不commit或者是长时间轮询,可能会占用较多的连接资源
0-2 分布式事务
1、一致性理论
分布式事务的目的是保障分库数据一致性,而跨库事务会遇到各种不可控制的问题,如个别节点永久性宕机,像单机事务一样的ACID是无法奢望的。另外,业界著名的CAP理论也告诉我们,对分布式系统,需要将数据的一致性、系统可用性、分区容忍性放在天平上一起考虑。
两阶段提交协议(简称2PC) 是实现分布式事务较为经典的方案,但2PC 的可扩展性很差,在分布式架构下应用代价较大,eBay 架构师Dan Pritchett 提出了BASE 理论,用于解决大规模分布式系统下的数据一致性问题。BASE 理论 告诉我们:可以通过放弃系统在每个时刻的强一致性来换取系统的可扩展性。
-
CAP理论
在分布式系统中,**一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)**三个要素最多只能同时满足两个,不可兼得。其中 分区容忍性 又是不可或缺的。
- 一致性:分布式环境下多个节点的数据是否强一致。
- 可用性:分布式服务能一直保证可用状态。当用户发出一个请求后,服务能在有限时间内返回结果。
- 分区容忍性:特指对网络分区的容忍性。即,系统中任意信息的丢失或失败不会影响系统的继续运作
-
BASE 理论
- 基本可用(Basically Available):指分布式系统在出现故障时,允许损失部分的可用性来保证核心可用。
- 软状态(Soft State):指允许分布式系统存在中间状态,该中间状态不会影响到系统的整体可用性。
- 最终一致性(Eventual Consistency):指分布式系统中的所有副本数据经过一定时间后,最终能够达到一致的状态。
2、两阶段提交(2PC)
二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即 将事务的提交过程分为准备阶段和提交阶段两个阶段来进行处理,通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
事务协调者(事务管理器):事务的发起者
事务参与者(资源管理器):事务的执行者
-
准备阶段(投票阶段)
协调者询问参与者事务是否执行成功,参与者发回事务执行结果,但该阶段并未提交事务。1)协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复;
2)各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务);
3)如果参与者执行成功,给协调者反馈同意,否则反馈终止。
-
提交阶段(执行阶段)
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。1)事务协调者节点向所有参与者节点发出
正式提交(commit)
的请求;
2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源;
3)参与者节点向协调者节点发送ACK完成消息;
4)事务协调者节点收到所有参与者节点反馈的ACK完成消息后,完成事务。
- 2PC优缺
-
优点
尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致,如宕机) -
缺点
-
性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
-
可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
-
数据一致性问题:二阶段无法解决的问题如协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
-
实现复杂:牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
-
-
3、三阶段提交(3PC)
三阶段提交协议是二阶段提交协议的改进版本,其有两个改动点。
1)在协调者和参与者中都引入超时机制;
2)在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
即除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit 和 DoCommit 三个阶段。
-
阶段1:CanCommit
- 事务询问:协调者向参与者发送一个CanCommit事务请求,询问是否可以执行事务提交操作,开始等待参与者的响应。
- 参与者向协调者反馈事务询问响应:参与者根据自身情况,反馈YES响应,进入预备状态,否则返回NO响应。
-
阶段2:PreCommit
-
返回yes情况:
- 1)协调者向所有参与者发送PreCommit请求,进入准备阶段;
- 2)参与者收到PreCommit请求后,执行事务操作,将undo和redo信息记录到日志中。
- 3)各参与者向协调者反馈事务执行响应:成功响应ACK,同时等待最终指令:提交还是终止。
-
返回no情况(中断事务):
- 1)任一参与者向协调者反馈了 NO响应 或者等待 超时 之后,协调者无法接收到所有参与者反馈响应,那么中断事务。
- 2)发送中断请求(abort请求)。
- 3)参与者收到协调者abort请求或者等待协调者请求超时,参与者中断事务。
-
-
阶段3:DoCommit
- 同步处理:
- 发送提交请求:协调者正常工作状态,接收到来自所有参与者的ACK响应,那么它将从预提交状态转换为提交状态,向所有参与者发送DoCommit请求。
- 事务提交:参与者收到DoCommit请求后,会正式执行事务提交操作,完成提交后,释放事务资源。
- 反馈事务提交结果:参与者完成事务提交之后,向协调者发送ACK消息。
- 完成事务:协调者收到所有参与者反馈的ACK消息后,完成事务。
- 回滚处理:
- 发送中断请求:协调者向所有参与者发送abort请求。
- 事务回滚:参与者收到abort请求后,利用 阶段2中的undo日志来执行事务回滚操作,完成回滚,释放资源。
- 反馈事务回滚结果:参与者完成了事务回滚后,向协调者发送ACK消息。
- 协调者收到所有参与者反馈的ACK消息后,中断事务。
- 同步处理:
4、Seata分布式事务方案
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
-
Seata术语
- TC(事务协调者):维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM(事务管理器):定义全局事务的范围:开始全局事务、提交或回滚全局事务
- RM(管理分支事务处理的资源):与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
-
Seata的2PC方案
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。拿不到全局锁 ,不能提交本地事务。
- 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
- 在数据库本地事务隔离级别读已提交或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交
- 如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
-
Seata执行流程分析
-
每个 RM 使用 DataSourceProxy 链接数据路,目的是使用 ConnectionProxy,使用数据源和数据代理的目的是在第一阶段将 undo_log 和业务数据放在一个本地事务提交,这样就保存了只要有业务操作就一定有 undo_log
-
在第一阶段 undo_log 中存放了数据修改前后修改后的值,为事务回滚做好准别,所以第一阶段完成就已经将分支事务提交了,也就释放了锁资源
-
TM 开启全局事务开始,将 XID全局事务ID 放在事务上下文中,通过 feign 调用也将 XID 传入下游分支事务,每个分支事务将自己的 Branch ID 分支事务ID与 XID 关联
-
第二阶段全局事务提交,TC会通知各分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各参与者只需要删除undo_log即可,并且可以异步执行,第二阶段很快可以完成
-
如果某一个分支事务异常,第二阶段就全局事务回滚操作,TC会通知各分支参与者回滚分支事务,通过 XID 和 Branch-ID 找到对应的回滚日志,通过回滚日志生成的反向SQL并执行,以完成分支事务回滚到之前
-
锁的实现原理
参考文章url:
锁的 happens-before关系
happens-before规则
- 程序顺序规则:在一个线程中,前面的操作 happens-before后面的操作
- 锁规则:对同一个锁,解锁 happens-before加锁。
- 传递性规则:A happens-before B,B happens-before C,则A happens-before C
从这段代码看看happens-before关系,线程A先执行store(),线程B后执行load()
这里有13个happens-before关系。①~⑤是线程A的程序顺序关系,⑥~⑩是线程B的程序顺序关系,⑪是锁规则关系,⑫~⑬是传递性关系
int value = 0;
boolean finish = 0;
//线程A
void store(){
//A:加锁前的操作
synchronized(this){ //B:加锁
value = 1; //C:写value
finish = true; //D:写finish
} //E:解锁
//F:解锁后的操作
}
//线程B
void load(){
//G:加锁前的操作
synchronized(this){ //H:加锁
if(finish){ //I:读finish
assert value == 1; //J:读value
}
} //K:解锁
//L:解锁后的操作
}
锁的图解:
从happens-before关系分析可见性
①~⑩根据程序顺序规则,只要不重排序数据依赖的指令,执行结果就是正确的,就可以保证在单线程内的可见性。
⑪根据锁规则,E happens-before H,也就是线程A解锁 happens-before 线程B加锁。
⑫根据传递性规则,线程A解锁前的操作都需要对线程B加锁可见,ABCDE happens-before H,也就是线程A解锁及其先前操作 happens-before 线程B加锁。
⑬再根据传递性规则,线程A解锁前的操作都需要对线程B加锁之后的操作可见,ABCDE happens-before HIJKL,最终得出线程A解锁及其先前操作 happens-before 线程B加锁及其后续操作。
这样来看,为了保证解锁及其之前操作的可见性,需要把解锁线程的本地内存刷新到主内存去。同时为了保证加锁线程读到最新的值,需要将本地内存的共享变量设为无效,重新从主内存中读取。
synchronized 锁的实现原理
参考url:
redis中有哪些锁?
分布式锁的特征:
- 互斥性:任意时刻,只有一个客户端能持有锁
- 锁超时释放: 持有锁超时,可以释放锁,防止死锁;
- 可重入锁: 一个线程获取了锁后,可以再次对其请求加锁;
- 高可用,高性能:加锁和解锁都需要开销尽可能低,同时也要保证高可用;
- 安全性:锁只能被持有的客户端删除,不能被其他持不到锁的客户删除
redis中的锁又是怎么样去实现的
锁有哪些类型?
对事务如何理解?
一、 Java集合
1.2 流程图关系
1.3 底层实现
ArrayList:底层是数组
LinkedList:底层是链表
Vector:底层是数组
HashSet:底层是哈希表
TreeSet:红黑树
HashMap:数组+链表
Hashtable:数组+链表
LinkedHashMap:数组+链表+红黑树
1.4 集合与数组的区别
1.4.1 元素类型
集合:引用类型(存储基本类型是自动装箱)
数组:基本类型、引用类型
1.4.2 元素个数
集合:不固定、可任意扩展
数组:固定,不能改变容量
1.5 集合的好处
不受容器大小限制,可以随时添加、删除元素,提供了大量操作元素的方法(判断、获取等)
List集合
List集合的特点:
可重复性(可以添加相同的元素)、有序(存取顺序相同)
List的主要方法有:
add、get、remove、set、iterator、contains、addAll、removeAll、indexOf、toArray、clear、isEmpty
1.6 List集合我们以ArrayList集合为例
ArrayList集合
java.util.ArrayList是大小可变的数组的实现,存储在内的数据称为元素。此类提供一些方法来操作内部存储的元素。ArrayList中可以不断添加元素,其大小也自动增长。
java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList集合是最常用的集合。
泛型:即泛指任意类型,幼教参数化类型,对具体类型的使用起到辅助作用,类似于方法的参数
1.7 迭代器的常用方法
next():返回迭代的下一个元素
hasNext():如果仍有元素可以迭代,则返回true
注意:列表迭代器是List体系独有的遍历方式,可以在对集合遍历的同时进行添加、删除等操作
但是必须通过调用列表迭代器的方法来实现
1.8 ArrayList和Vector的区别
-
线程同步,Vector线程安全,ArrayList线程不安全,因为Vector的实现有synchronized锁
-
效率问题,Vector效率低,ArrayList效率高
-
增长数量,Vector以2倍增长,ArrayList以1.5倍增长
1.8.1 说出ArrayList,Vector, LinkedList的存储性能和特性
(1) ArrayList和Vector使用数组存储元素;LinkedList使用链表存储元素
(2) ArrayList和Vector插入删除数据时,需要搬运数据,效率较差;LinkedList使用链表,不需要搬运数据,效率高。
(3) ArrayList和Vectory查询时,按数组下标查询,不需要遍历,效率高;LinkedList需要遍历,查询效率底。
1.8.2 多线程场景下如何使用 ArrayList?
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
1.8.3 为什么 ArrayList 的 elementData 加上 transient 修饰?
ArrayList 中的数组定义如下:
private transient Object[] elementData;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
*// Write out element count, and any hidden stuff*
int expectedModCount = modCount;
s.defaultWriteObject();
*// Write out array length*
s.writeInt(elementData.length);
*// Write out all elements in the proper order.*
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。
1.9 Set集合的特点:
不可重复(元素具有唯一性)、无序(元素的存取顺序是不固定的)
Set的主要方法有:
add、remove、iterator、contains、addAll、removeAll、toArray、clear、isEmpty
HashSet:内部的数据结构是哈希表,是线程不安全的。
HashSet中保证集合中元素是唯一的方法:通过对象的hashCode和equals方法来完成对象唯一性的判断。
如果对象的hashCode值不同,则不用判断equals方法,就直接存到HashSet中。
如果对象的hashCode值相同,需要用equals方法进行比较,如果结果为true,则视为相同元素,不存,如果结果为false,视为不同元素,进行存储。
注意:如果元素要存储到HashCode中,必须覆盖hashCode方法和equals方法。
TreeSet:可以对Set集合中的元素进行排序,是线程不安全的。
**TreeSet:**中判断元素唯一性的方法是:根据比较方法的返回结果是否为0,如果是0,则是相同元素,不存,如果不是0,则是不同元素,存储。
1.9.1 说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
1.9.2 HashSet如何检查重复?HashSet是如何保证数据不可重复的?
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
以下是HashSet 部分源码:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
1.10 TreeSet对元素进行排序的方式:
元素自身具备比较功能,即自然排序,需要实现Comparable接口,并覆盖其compareTo方法。
元素自身不具备比较功能,则需要实现Comparator接口,并覆盖其compare方法。
注意:LinkedHashSet是一种有序的Set集合,即其元素的存入和输出的顺序是相同的。
1.11 List,Set,Map集合的特点 & 区别
List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。
Collection集合主要有List和Set两大接口
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
关系图如下:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/98a9a972f387104ebee5fa12adaa5c26.png)
1.12 HashMap和Hashtable的区别
-
线程同步,Hashtable线程安全,HashMap线程不安全
-
效率问题,Hashtable效率低,HashMap效率高
-
HashMap可以使用null作为key,Hashtable不可以使用null为key
-
HashMap使用的是新实现,继承AbstractMap,而Hashtable是继承Dictionary类,实现比较老
-
Hash算法不同,HashMap的hash算法比Hashtable的hash算法效率高
-
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey。因为contains方法容易让人引起误解。
-
取值不同,HashMap用的是Iterator接口,而Hashtable中还有使用Enumeration接口
1.3 HashSet 与 HashMap的区别
1.4 说一下HashMap的实现原理(非常重要)
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
①HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
HashMap存储结构
这里需要区分一下,JDK1.7和 JDK1.8之后的 HashMap 存储结构。在JDK1.7及之前,是用数组加链表的方式存储的。
但是,众所周知,当链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n)。因此,JDK1.8 把它设计为达到一个特定的阈值之后,就将链表转化为红黑树。
这里简单说下红黑树的特点:
每个节点只有两种颜色:红色或者黑色
根节点必须是黑色
每个叶子节点(NIL)都是黑色的空节点
从根节点到叶子节点,不能出现两个连续的红色节点
从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相同
由于红黑树,是一个自平衡的二叉搜索树,因此可以使查询的时间复杂度降为O(logn)。(红黑树不是本文重点,不了解的童鞋可自行查阅相关资料哈)
常用的变量
在 HashMap源码中,比较重要的常用变量,主要有以下这些。还有两个内部类来表示普通链表的节点和红黑树节点
//默认的初始化容量为16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。
//为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。
//若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低,
//若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上的元素个数,减少到6个时,就退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。
//这是为了避免,数组扩容和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
//存放所有Node节点的数组
transient Node<K,V>[] table;
//存放所有的键值对
transient Set<Map.Entry<K,V>> entrySet;
//map中的实际键值对个数,即数组中元素个数
transient int size;
//每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。
//当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
transient int modCount;
//数组扩容阈值
int threshold;
//加载因子
final float loadFactor;
//普通单向链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
final int hash;
final K key;
V value;
//指向单链表的下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
//转化为红黑树的节点类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//当前节点的父节点
TreeNode<K,V> parent;
//左孩子节点
TreeNode<K,V> left;
//右孩子节点
TreeNode<K,V> right;
//指向前一个节点
TreeNode<K,V> prev; // needed to unlink next upon deletion
//当前节点是红色或者黑色的标识
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
HashMap 构造函数
HashMap有四个构造函数可供我们使用,一起来看下:
//默认无参构造,指定一个默认的加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//可指定容量的有参构造,但是需要注意当前我们指定的容量并不一定就是实际的容量,下面会说
public HashMap(int initialCapacity) {
//同样使用默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16
//注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?
//先卖个关子,等到 resize 的时候再说
this.threshold = tableSizeFor(initialCapacity);
}
//可传入一个已有的map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//把传入的map里边的元素都加载到当前map
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//put方法的具体实现,后边讲
putVal(hash(key), key, value, false, evict);
}
}
}
tableSizeFor()
上边的第三个构造函数中,调用了 tableSizeFor 方法,这个方法是怎么实现的呢?
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
我们以传入参数为14 来举例,计算这个过程。
首先,14传进去之后先减1,n此时为13。然后是一系列的无符号右移运算。
//13的二进制
0000 0000 0000 0000 0000 0000 0000 1101
//无右移1位,高位补0
0000 0000 0000 0000 0000 0000 0000 0110
//然后把它和原来的13做或运算得到,此时的n值
0000 0000 0000 0000 0000 0000 0000 1111
//再以上边的值,右移2位
0000 0000 0000 0000 0000 0000 0000 0011
//然后和第一次或运算之后的 n 值再做或运算,此时得到的n值
0000 0000 0000 0000 0000 0000 0000 1111
...
//我们会发现,再执行右移 4,8,16位,同样n的值不变
//当n小于0时,返回1,否则判断是否大于最大容量,是的话返回最大容量,否则返回 n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
//很明显我们这里返回的是 n+1 的值,
0000 0000 0000 0000 0000 0000 0000 1111
+ 1
0000 0000 0000 0000 0000 0000 0001 0000
将它转为十进制,就是 2^4 = 16 。我们会发现一个规律,以上的右移运算,最终会把最低位的值都转化为 1111 这样的结构,然后再加1,就是1 0000 这样的结构,它一定是 2的n次幂。因此,这个方法返回的就是大于当前传入值的最小(最接近当前值)的一个2的n次幂的值。
put()方法详解
//put方法,会先调用一个hash()方法,得到当前key的一个hash值,
//用于确定当前key应该存放在数组的哪个下标位置
//这里的 hash方法,我们姑且先认为是key.hashCode(),其实不是的,一会儿细讲
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//把hash值和当前的key,value传入进来
//这里onlyIfAbsent如果为true,表明不能修改已经存在的值,因此我们传入false
//evict只有在方法 afterNodeInsertion(boolean evict) { }用到,可以看到它是一个空实现,因此不用关注这个参数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table是否为空,如果空的话,会先调用resize扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据当前key的hash值找到它在数组中的下标,判断当前下标位置是否已经存在元素,
//若没有,则把key、value包装成Node节点,直接添加到此位置。
// i = (n - 1) & hash 是计算下标位置的,为什么这样算,后边讲
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果当前位置已经有元素了,分为三种情况。
Node<K,V> e; K k;
//1.当前位置元素的hash值等于传过来的hash,并且他们的key值也相等,
//则把p赋值给e,跳转到①处,后续需要做值的覆盖处理
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//2.如果当前是红黑树结构,则把它加入到红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//3.说明此位置已存在元素,并且是普通链表结构,则采用尾插法,把新节点加入到链表尾部
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//如果头结点的下一个节点为空,则插入新节点
p.next = newNode(hash, key, value, null);
//如果在插入的过程中,链表长度超过了8,则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//插入成功之后,跳出循环,跳转到①处
break;
}
//若在链表中找到了相同key的话,直接退出循环,跳转到①处
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//①
//说明发生了碰撞,e代表的是旧值,因此节点位置不变,但是需要替换为新值
if (e != null) { // existing mapping for key
V oldValue = e.value;
//用新值替换旧值,并返回旧值。
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//看方法名字即可知,这是在node被访问之后需要做的操作。其实此处是一个空实现,
//只有在 LinkedHashMap才会实现,用于实现根据访问先后顺序对元素进行排序,hashmap不提供排序功能
// Callbacks to allow LinkedHashMap post-actions
//void afterNodeAccess(Node<K,V> p) { }
afterNodeAccess(e);
return oldValue;
}
}
//fail-fast机制
++modCount;
//如果当前数组中的元素个数超过阈值,则扩容
if (++size > threshold)
resize();
//同样的空实现
afterNodeInsertion(evict);
return null;
}
hash()计算原理
前面 put 方法中说到,需要先把当前key进行哈希处理,我们看下这个方法是怎么实现的。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
运行一段程序,把它的 hashCode的二进制打印出来,如下。
public static void main(String[] args) {
Object o = new Object();
int hash = o.hashCode();
System.out.println(hash);
System.out.println(Integer.toBinaryString(hash));
}
//1836019240
//1101101011011110110111000101000
然后,进行 (h = key.hashCode()) ^ (h >>> 16) 这一段运算。
//h原来的值
0110 1101 0110 1111 0110 1110 0010 1000
//无符号右移16位,其实相当于把低位16位舍去,只保留高16位
0000 0000 0000 0000 0110 1101 0110 1111
//然后高16位和原 h进行异或运算
0110 1101 0110 1111 0110 1110 0010 1000
^
0000 0000 0000 0000 0110 1101 0110 1111
=
0110 1101 0110 1111 0000 0011 0100 0111
resize() 扩容机制
在上边 put 方法中,我们会发现,当数组为空的时候,会调用 resize 方法,当数组的 size 大于阈值的时候,也会调用 resize方法。 那么看下 resize 方法都做了哪些事情吧。
final Node<K,V>[] resize() {
//旧数组
Node<K,V>[] oldTab = table;
//旧数组的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧数组的扩容阈值,注意看,这里取的是当前对象的 threshold 值,下边的第2种情况会用到。
int oldThr = threshold;
//初始化新数组的容量和阈值,分三种情况讨论。
int newCap, newThr = 0;
//1.当旧数组的容量大于0时,说明在这之前肯定调用过 resize扩容过一次,才会导致旧容量不为0。
//为什么这样说呢,之前我在 tableSizeFor 卖了个关子,需要注意的是,它返回的值是赋给了 threshold 而不是 capacity。
//我们在这之前,压根就没有在任何地方看到过,它给 capacity 赋初始值。
if (oldCap > 0) {
//容量达到了最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新数组的容量和阈值都扩大原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//2.到这里,说明 oldCap <= 0,并且 oldThr(threshold) > 0,这就是 map 初始化的时候,第一次调用 resize的情况
//而 oldThr的值等于 threshold,此时的 threshold 是通过 tableSizeFor 方法得到的一个2的n次幂的值(我们以16为例)。
//因此,需要把 oldThr 的值,也就是 threshold ,赋值给新数组的容量 newCap,以保证数组的容量是2的n次幂。
//所以我们可以得出结论,当map第一次 put 元素的时候,就会走到这个分支,把数组的容量设置为正确的值(2的n次幂)
//但是,此时 threshold 的值也是2的n次幂,这不对啊,它应该是数组的容量乘以加载因子才对。别着急,这个会在③处理。
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//3.到这里,说明 oldCap 和 oldThr 都是小于等于0的。也说明我们的map是通过默认无参构造来创建的,
//于是,数组的容量和阈值都取默认值就可以了,即 16 和 12。
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//③ 这里就是处理第2种情况,因为只有这种情况 newThr 才为0,
//因此计算 newThr(用 newCap即16 乘以加载因子 0.75,得到 12) ,并把它赋值给 threshold
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//赋予 threshold 正确的值,表示数组下次需要扩容的阈值(此时就把原来的 16 修正为了 12)。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//我们可以发现,在构造函数时,并没有创建数组,在第一次调用put方法,导致resize的时候,才会把数组创建出来。这是为了延迟加载,提高效率。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果原来的数组不为空,那么我们就需要把原来数组中的元素重新分配到新的数组中
//如果是第2种情况,由于是第一次调用resize,此时数组肯定是空的,因此也就不需要重新分配元素。
if (oldTab != null) {
//遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//取到当前下标的第一个元素,如果存在,则分三种情况重新分配位置
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//1.如果当前元素的下一个元素为空,则说明此处只有一个元素
//则直接用它的hash()值和新数组的容量取模就可以了,得到新的下标位置。
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//2.如果是红黑树结构,则拆分红黑树,必要时有可能退化为链表
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//3.到这里说明,这是一个长度大于 1 的普通链表,则需要计算并
//判断当前位置的链表是否需要移动到新的位置
else { // preserve order
// loHead 和 loTail 分别代表链表旧位置的头尾节点
Node<K,V> loHead = null, loTail = null;
// hiHead 和 hiTail 分别代表链表移动到新位置的头尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//如果当前元素的hash值和oldCap做与运算为0,则原位置不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//否则,需要移动到新的位置
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//原位置不变的一条链表,数组下标不变
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//移动到新位置的一条链表,数组下标为原下标加上旧数组的容量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上边还有一个非常重要的运算,我们没有讲解。就是下边这个判断,它用于把原来的普通链表拆分为两条链表,位置不变或者放在新的位置。
if ((e.hash & oldCap) == 0) {} else {}
get()方法
有了前面的基础,get方法就比较简单了。
public V get(Object key) {
Node<K,V> e;
//如果节点为空,则返回null,否则返回节点的value。这也说明,hashMap是支持value为null的。
//因此,我们就明白了,为什么hashMap支持Key和value都为null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//首先要确保数组不能为空,然后取到当前hash值计算出来的下标位置的第一个元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//若hash值和key都相等,则说明我们要找的就是第一个元素,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不是的话,就遍历当前链表(或红黑树)
if ((e = first.next) != null) {
//如果是红黑树结构,则找到当前key所在的节点位置
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果是普通链表,则向后遍历查找,直到找到或者遍历到链表末尾为止。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//否则,说明没有找到,返回null
return null;
}
为什么HashMap链表会形成死循环
准确的讲应该是 JDK1.7 的 HashMap 链表会有死循环的可能,因为JDK1.7是采用的头插法,在多线程环境下有可能会使链表形成环状,从而导致死循环。JDK1.8做了改进,用的是尾插法,不会产生死循环。
那么,链表是怎么形成环状的呢?
关于这一点的解释,我发现网上文章抄来抄去的,而且都来自左耳朵耗子,更惊奇的是,连配图都是一模一样的。(别问我为什么知道,因为我也看过耗子叔的文章,哈哈。然而,菜鸡的我,那篇文章,并没有看懂。。。)
我实在看不下去了,于是一怒之下,就有了这篇文章。我会照着源码一步一步的分析变量之间的关系怎么变化的,并有配图哦。
我们从 put()方法开始,最终找到线程不安全的那个方法。这里省略中间不重要的过程,我只把方法的跳转流程贴出来:
//添加元素方法 -> 添加新节点方法 -> 扩容方法 -> 把原数组元素重新分配到新数组中
put() --> addEntry() --> resize() --> transfer()
问题就发生在 transfer 这个方法中。
我们假设,原数组容量只有2,其中一条链表上有两个元素 A,B,如下图
现在,有两个线程都执行 transfer 方法。每个线程都会在它们自己的工作内存生成一个newTable 的数组,用于存储变化后的链表,它们互不影响(这里互不影响,指的是两个新数组本身互不影响)。但是,需要注意的是,它们操作的数据却是同一份。
因为,真正的数组中的内容在堆中存储,它们指向的是同一份数据内容。就相当于,有两个不同的引用 X,Y,但是它们都指向同一个对象 Z。这里 X、Y就是两个线程不同的新数组,Z就是堆中的A,B 等元素对象。
假设线程一执行到了上图1中所指的代码①处,恰好 CPU 时间片到了,线程被挂起,不能继续执行了。 记住此时,线程一中记录的 e = A , e.next = B。
然后线程二正常执行,扩容后的数组长度为 4, 假设 A,B两个元素又碰撞到了同一个桶中。然后,通过几次 while 循环后,采用头插法,最终呈现的结构如下:
此时,线程一解挂,继续往下执行。注意,此时线程一,记录的还是 e = A,e.next = B,因为它还未感知到最新的变化。
我们主要关注图1中标注的①②③④处的变量变化:
/**
* next = e.next
* e.next = newTable[i]
* newTable[i] = e;
* e = next;
*/
//第一次循环,(伪代码)
e=A;next=B;
e.next=null //此时线程一的新数组刚初始化完成,还没有元素
newTab[i] = A->null //把A节点头插到新数组中
e=B; //下次循环的e值
第一次循环结束后,线程一新数组的结构如下图:
然后,由于 e=B,不为空,进入第二次循环。
//第二次循环
e=B;next=A; //此时A,B的内容已经被线程二修改为 B->A->null,然后被线程一读到,所以B的下一个节点指向A
e.next=A->null // A->null 为第一次循环后线程一新数组的结构
newTab[i] = B->A->null //新节点B插入之后,线程一新数组的结构
e=A; //下次循环的 e 值
第二次循环结束后,线程一新数组的结构如下图:
此时,由于 e=A,不为空,继续循环。
这时,有的同学可能就会问了,就算他们成环了,又怎样,跟死循环有什么关系?
我们看下 get() 方法(最终调用 getEntry 方法),
可以看到查找元素时,只要 e 不为空,就会一直循环查找下去。若有某个元素 C 的 hash 值也落在了和 A,B元素同一个桶中,则会由于, A,B互相指向,e.next 永远不为空,就会形成死循环。
1.4.1 HashMap 基于 Hash 算法实现的
当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
1.5 HashMap的扩容操作是怎么实现的?
HashMap长度是默认的16,length - 1的结果 : 十进制 : 15
具体实现机制:参考 hashMap扩容机制
①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
②.每次扩展的时候,都是扩展2倍;
③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
1.6 HashMap是怎么解决哈希冲突的?
答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;
1.7 什么是哈希?
Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
1.8 所有散列函数都有如下一个基本特性
根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
1.9 什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
二、 Java基础
自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
基本数据类型与包装类的区别(int 和 Integer 有什么区别)
1、Integer是int的包装类,int则是java的一种基本数据类型
2、Integer变量必须实例化后才能使用,而int变量不需要
3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
4、Integer的默认值是null,int的默认值是0
应用场景的区别:
比如要体现出 考试成绩为0和缺考的区别的时候 用Integer可以 int不行
比如用容器的时候 ,ArrayList等职能放对象,不能放基本数据类型。
将基本数据类型封装成对象的好处是:
1)、在对象中可以定义更多的功能方法操作该数据。例如:基本数据类型和字符串直接的转换。
2)、编码过程中只接收对象的情况,例如List中只存入对象,不能存入基本数据类型。
3、使用场景
大部分的情况下,这两种类型没有太大得区别。根据以上两点的分析,基本类型的存取速度会更快,对象中有更多功能方法来操作数据,要根据实际需要定义属性。
借鉴网上学生成绩的例子,没来考试,成绩是0还是null,如果你觉得是0就用int,如果你认为是null,就用Integer。
堆和栈的区别
那数据存放在堆中和栈中有什么区别呢?
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用(堆栈分离的好处:))。
为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java中参数传递时的问题。
栈:
1)栈的存取速度比堆快,仅次于直接位于CPU的寄存器。
2)栈中的数据的大小和生存周期是确定的。
3)栈中的数据可以共享。
堆:
1)堆可以动态的分配内存大小,生存期也不必告诉编译器。
2)堆在运行时动态分配内存,存取速度慢。
综上所述,可以简单的理解为,为了高效,可以把一些数值小,简单的变量存放在栈中。堆和栈
堆(Heap)与栈(Stack)是开发人员必须面对的两个概念,在理解这两个概念时,需要放到具体的场景下,因为不同场景下,堆与栈代表不同的含义。一般情况下,有两层含义:
(1)程序内存布局场景下,堆与栈表示两种内存管理方式;
(2)数据结构场景下,堆与栈表示两种常用的数据结构。
在说堆和栈之前,我们先说一下JVM(虚拟机)内存的划分:
Java程序在运行时都要开辟空间,任何软件在运行时都要在内存中开辟空间,Java虚拟机运行时也是要开辟空间的。JVM运行时在内存中开辟一片内存区域,启动时在自己的内存区域中进行更细致的划分,因为虚拟机中每一片内存处理的方式都不同,所以要单独进行管理。
JVM内存的划分有五片:
1. 寄存器;
2. 本地方法区;
3. 方法区;
4. 栈内存;
5. 堆内存。
重点来说一下堆和栈:
栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
下面我们通过一个图例详细讲一下堆和栈:
比如主函数里的语句 int [] arr=new int [3];在内存中是怎么被定义的:
主函数先进栈,在栈中定义一个变量arr,接下来为arr赋值,但是右边不是一个具体值,是一个实体。实体创建在堆里,在堆里首先通过new关键字开辟一个空间,内存在存储数据的时候都是通过地址来体现的,地址是一块连续的二进制,然后给这个实体分配一个内存地址。数组都是有一个索引,数组这个实体在堆内存中产生之后每一个空间都会进行默认的初始化(这是堆内存的特点,未初始化的数据是不能用的,但在堆里是可以用的,因为初始化过了,但是在栈里没有),不同的类型初始化的值不一样。所以堆和栈里就创建了变量和实体:
那么堆和栈是怎么联系起来的呢?
我们刚刚说过给堆分配了一个地址,把堆的地址赋给arr,arr就通过地址指向了数组。所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它。这种我们不再叫他基本数据类型,而叫引用数据类型。称为arr引用了堆内存当中的实体。(可以理解为c或c++的指针,Java成长自c++和c++很像,优化了c++)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/68012f6840c2f784aa9b458980a3e5bb.png)
如果当int [] arr=null;
arr不做任何指向,null的作用就是取消引用数据类型的指向。
当一个实体,没有引用数据类型指向的时候,它在堆内存中不会被释放,而被当做一个垃圾,在不定时的时间内自动回收,因为Java有一个自动回收机制,(而c++没有,需要程序员手动回收,如果不回收就越堆越多,直到撑满内存溢出,所以Java在内存管理上优于c++)。自动回收机制(程序)自动监测堆里是否有垃圾,如果有,就会自动的做垃圾回收的动作,但是什么时候收不一定。
堆与栈的区别 很明显:
1.栈内存存储的是局部变量而堆内存存储的是实体;
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
延伸:关于Integer和int的比较
1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false
2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true
3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
对于第4条的原因:
java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
Integer a= 127 与 Integer b = 127相等吗
对于对象引用类型:==比较的是对象的内存地址。
对于基本数据类型:==比较的是值。
如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false
Double和double的区别
1、Double是java定义的类,而double是预定义数据类型(8种中的一种)
2、Double就好比是对double类型的封装,内置很多方法可以实现String到double的转换,以及获取各种double类型的属性值(MAX_VALUE、SIZE等等)
基于上述两点,如果你在普通的定义一个浮点类型的数据,两者都可以,但是Double是类所以其对象是可以为NULL的,而double定义的不能为NULL,如果你要将一些数字字符串,那么就应该使用Double类型了,其内部帮你实现了强转。
面向对象和面向过程的区别
面向过程:
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低
2.1 面向对象三大特性
2.1.1 封装 继承 多态
抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
2.1.2 其中Java 面向对象编程三大特性:封装 继承 多态
封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承可以提高代码复用性。继承是多态的前提。
2.1.3 关于继承如下 3 点请记住:
子类拥有父类非 private 的属性和方法。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。
一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
方法重写(子类继承父类并重写父类中已有的或抽象的方法);
对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
2.1.4 什么是多态机制?Java语言是如何实现多态的?
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
2.1.5 Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
2.2 类与接口
2.2.1 抽象类和接口的对比
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
2.2.3 普通类和抽象类有哪些区别?
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
2.2.4 抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类
2.2.5 重写与重载
构造器(constructor)是否可被重写(override)
构造器不能被继承,因此不能被重写,但可以被重载。
2.2.6 重载(Overload)和重写(Override)的区别。
重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
2.2.7 对象相等判断
(1) == 和 equals 的区别是什么
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
说明:
String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
2.2.8 hashCode 与 equals (重要)
(1) HashSet如何检查重复
两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
(2) hashCode和equals方法的关系
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
hashCode()介绍
(3) hashCode() 的作用
获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
2.3 反射
2.3.1 什么是反射机制?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
静态编译和动态编译
**静态编译:**在编译时确定类型,绑定对象
**动态编译:**运行时确定类型,绑定对象
2.3.2 反射机制优缺点
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
2.3.3 请简介你对Java反射的理解以及使用场景
Java反射指的是在运行状态中对任意一个类,都能够知道这个类的所有属性和方法;对于任意一个的对象,都能调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java反射;
Java反射使用的场景:在Java编码时知道类和对象的具体信息,此时直接对类和对象进行操作即可;在Java编码时不知道类和对象的具体信息时,此时使用Java反射来获取
3. io流
java 中 IO 流分为几种?
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。
Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
IO流主要的分类方式有以下3种:
-
按数据流的方向:输入流、输出流
-
按处理数据单位:字节流、字符流
-
按功能:节点流、处理流
输入流与输出流
输入与输出是相对于应用程序而言的,比如文件读写,读取文件是输入流,写文件是输出流,这点很容易搞反。
字节流与字符流
字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。
字节流和字符流的其他区别:
字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。详见文末效率对比。
节点流和处理流
节点流:直接操作数据读写的流类,比如FileInputStream
处理流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能,例如BufferedInputStream(缓冲字节流)
按操作方式分类结构图:
按操作对象分类结构图:
BIO,NIO,AIO 有什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
详细分析
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
Files的常用方法都有哪些?
Files. exists():检测文件路径是否存在。
Files. createFile():创建文件。 从
Files. createDirectory():创建文件夹。
Files. delete():删除一个文件或目录。
Files. copy():复制文件。
Files. move():移动文件。
Files. size():查看文件个数。
Files. read():读取文件。
Files. write():写入文件。
4. String
字符型常量和字符串常量的区别
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)
什么是字符串常量池?
字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
String 是最基本的数据类型吗
不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char[] chars = {‘你’,‘好’};
但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。
String有哪些特性
不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
String为什么是不可变的吗?
简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下所示:
/** The value is used for character storage. */
private final char value[];
String类被final修饰,不可被继承!
如何将字符串反转?
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
数组有没有 length()方法?String 有没有 length()方法
数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。
String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
String和StringBuffer、StringBuilder的区别?
String为什么是不可变的?
可变性
String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。
线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结
如果要操作少量的数据用 = String
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer
5. maven常用打包命令
1、mvn compile 编译,将Java 源程序编译成 class 字节码文件。
2、mvn test 测试,并生成测试报告
3、mvn clean 将以前编译得到的旧的 class 字节码文件删除
4、mvn pakage 打包,动态 web工程打 war包,Java工程打 jar 包。
5、mvn install 将项目生成 jar 包放在仓库中,以便别的模块调用
6、mvn clean install -Dmaven.test.skip=true 抛弃测试用例打包
总结
Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
下面再总结一下Java中方法参数的使用情况:
一个方法不能修改一个基本数据类型的参数(即数值型或布尔型》
一个方法可以改变一个对象参数的状态。
一个方法不能让对象参数引用一个新的对象。
值传递和引用传递有什么区别
**值传递:**指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
**引用传递:**指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
Java包 ,JDK 中常用的包有哪些
**java.lang:**这个是系统的基础类;
**java.io:**这里面是所有输入输出有关的类,比如文件操作等;
**java.nio:**为了完善 io 包中的功能,提高 io 包中性能而写的一个新包;
**java.net:**这里面是与网络有关的类;
**java.util:**这个是系统辅助类,特别是集合类;
**java.sql:**这个是数据库操作的类。
import java和javax有什么区别
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。
所以,实际上java和javax没有区别。这都是一个名字。
三、 多线程
-1 高并发
参考文章: 【多线程高并发编程】二 实现多线程的几种方式
〇、使用多线程的场景
1. 为什么使用多线程
通俗的解释一下多线程先:
多线程用于堆积处理,就像一个大土堆,一个推土机很慢,那么10个推土机一起来处理,当然速度就快了,不过由于位置的限制,如果20个推土机,那么推土机之间会产生相互的避让,相互摩擦,相互拥挤,反而不如10个处理的好,所以,多线程处理,线程数要开的恰当,就可以提高效率。
多线程使用的目的:
1、吞吐量:做WEB,容器帮你做了多线程,但是它只能帮你做请求层面的,简单的说,就是一个请求一个线程(如struts2,是多线程的,每个客户端请求创建一个实例,保证线程安全),或多个请求一个线程,如果是单线程,那只能是处理一个用户的请求。
2、伸缩性:通过增加CPU核数来提升性能。
多线程的使用场景:
1、常见的浏览器、Web服务(现在写的web是中间件帮你完成了线程的控制),web处理请求,各种专用服务器(如游戏服务器)
2、servlet多线程
3、FTP下载,多线程操作文件
4、数据库用到的多线程
5、分布式计算
6、tomcat,tomcat内部采用多线程,上百个客户端访问同一个WEB应用,tomcat接入后就是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用我们的servlet程序,比如doGet或者dpPost方法
7、后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集
8、自动作业处理:比如定期备份日志、定期备份数据库
9、异步处理:如发微博、记录日志
10、页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)
11、数据库的数据分析(待分析的数据太多),数据迁移
12、多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成
1. 线程概述
1.1 线程和进程
进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
线程也被称为轻量级进程,线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其它线程共享该进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
1.2 并发和并行
并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
1.3 多线程的优势
(1)进程之间不能共享内存,但线程之间共享内存却非常容易。
(2)系统创建进程时需要为该进程重新分配系统资源,但创建线程代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
(3)java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程。
1.4 程序运行原理
分时调度:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
1.5 主线程
jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。
2. 线程的创建和启动
2.1 Thread类
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。Java使用县城执行体来表示这段流。
2.2创建线程有哪几种方法
2.2.1 继承Thread类,重写Run方法(其中Thread类本身也是实现了Runnable接口)
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
示例代码:
package com.thread;
public class FirstThreadTest extends Thread{
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run(){
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args){
for(int i = 0;i< 100;i++){
System.out.println(Thread.currentThread().getName()+" : "+i);
if(i==20){
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
2.2.2 实现Runnable接口,重写run方法
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
示例代码:
public class RunnableThreadTest implements Runnable{
private int i;
public void run() {
for(i = 0;i <100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args){
for(int i = 0;i < 100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20) {
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt,"新线程1").start();
new Thread(rtt,"新线程2").start();
}
}
}
}
2.2.3 实现 Callable 接口,重写 call方法(有返回值)
通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现**call()方法,该call()**方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的**call()**方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
实例代码:
package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args) {
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
if (i == 20) {
new Thread(ft, "有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:" + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
}
2.2.4 通过线程池创建线程
创建线程的三种方式的对比
- 采用实现Runnable、Callable接口的方式创见多线程时
优势:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。 - 使用继承Thread类的方式创建多线程时
优势:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势:
线程类已经继承了Thread类,所以不能再继承其他父类。
3. 线程的 6 种状态
就像生物从出生到长大、最终死亡的过程一样,线程也有自己的生命周期,在 Java 中线程的生命周期中一共有 6 种状态。
-
New(新创建)
-
Runnable(可运行)
-
Blocked(被阻塞)
-
Waiting(等待)
-
Timed Waiting(计时等待)
-
Terminated(被终止)
如果想要确定线程当前的状态,可以通过 getState() 方法,并且线程在任何时刻只可能处于 1 种状态。
运行状态可能会有阻塞:
四、Spring
什么是spring?
Spring是一个轻量级Java开发框架,最早有Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。
Spring最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。
Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP)。
为了降低Java开发的复杂性,Spring采取了以下4种关键策略
基于POJO的轻量级和最小侵入性编程;
通过依赖注入和面向接口实现松耦合;
基于切面和惯例进行声明式编程;
通过切面和模板减少样板式代码。
Spring框架的设计目标,设计理念,和核心是什么
Spring设计目标:Spring为开发者提供一个一站式轻量级应用开发平台;
Spring设计理念:在JavaEE开发中,支持POJO和JavaBean开发方式,使应用面向接口开发,充分支持OO(面向对象)设计方法;Spring通过IoC容器实现对象耦合关系的管理,并实现依赖反转,将对象之间的依赖关系交给IoC容器,实现解耦;
Spring框架的核心:IoC容器和AOP模块。通过IoC容器管理POJO对象以及他们之间的耦合关系;通过AOP以动态非侵入的方式增强服务。
IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。
spring常用的注解
什么是基于Java的Spring注解配置? 给一些注解的例子
基于Java的配置,允许你在少量的Java注解的帮助下,进行你的大部分Spring配置而非通过XML文件。
以@Configuration 注解为例,它用来标记类可以当做一个bean的定义,被Spring IOC容器使用。
另一个例子是@Bean注解,它表示此方法将要返回一个对象,作为一个bean注册进Spring应用上下文。
@Configuration
public class StudentConfig {
@Bean
public StudentBean myStudent() {
return new StudentBean();
}
}
怎样开启注解装配?
注解装配在默认情况下是不开启的,为了使用注解装配,我们必须在Spring配置文件中配置 context:annotation-config/元素。
@Component, @Controller, @Repository, @Service 有何区别?
@Component:这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。
@Controller:这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。
@Service:此注解是组件注解的特化。它不会对 @Component 注解提供任何其他行为。您可以在服务层类中使用 @Service 而不是 @Component,因为它以更好的方式指定了意图。
@Repository:这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。
@Required 注解有什么作用 ?
这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显式的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializationException。示例:
public class Employee {
private String name;
@Required
public void setName(String name){
this.name=name;
}
public string getName(){
return name;
}
}
@Autowired 注解有什么作用 ?
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。@Autowired 注解提供了更细粒度的控制,包括在何处以及如何完成自动装配。它的用法和@Required一样,修饰setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。
public class Employee {
private String name;
@Autowired
public void setName(String name) {
this.name=name;
}
public string getName(){
return name;
}
}
@Autowired和@Resource之间的区别
@Autowired可用于:构造函数、成员变量、Setter方法
@Autowired和@Resource之间的区别
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
@Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
@Qualifier 注解有什么作用
当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。
@RequestMapping 注解有什么用?
@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注释可应用于两个级别:
类级别:映射请求的 URL
方法级别:映射 URL 以及 HTTP 请求方法
Spring的优缺点是什么?
优点
方便解耦,简化开发
Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给Spring管理。
AOP编程的支持
Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
声明式事务的支持
只需要通过配置就可以完成对事务的管理,而无需手动编程。
方便程序的测试
Spring对Junit4支持,可以通过注解方便的测试Spring程序。
方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。
降低JavaEE API的使用难度
Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低。
缺点
Spring明明一个很轻量级的框架,却给人感觉大而全
Spring依赖反射,反射影响性能
使用门槛升高,入门Spring需要较长时间
Spring有哪些应用场景
应用场景:JavaEE企业应用开发,包括SSH、SSM等
Spring价值:
Spring是非侵入式的框架,目标是使应用程序代码对框架依赖最小化;
Spring提供一个一致的编程模型,使应用直接使用POJO开发,与运行环境隔离开来;
Spring推动应用设计风格向面向对象和面向接口开发转变,提高了代码的重用性和可测试性;
Spring由哪些模块组成?
Spring 总共大约有 20 个模块, 由 1300 多个不同的文件构成。 而这些组件被分别整合在核心容器(Core Container) 、 AOP(Aspect Oriented Programming)和设备支持(Instrmentation) 、数据访问与集成(Data Access/Integeration) 、 Web、 消息(Messaging) 、 Test等 6 个模块中。 以下是 Spring 5 的模块结构图:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/04a8e6a546a57383a802a1c902d30475.png)
spring core:提供了框架的基本组成部分,包括控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)功能。
spring beans:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管理对象称为Bean。
spring context:构建于 core 封装包基础上的 context 封装包,提供了一种框架式的对象访问方法。
spring jdbc:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析, 用于简化JDBC。
spring aop:提供了面向切面的编程实现,让你可以自定义拦截器、切点等。
spring Web:提供了针对 Web 开发的集成特性,例如文件上传,利用 servlet listeners 进行 ioc 容器初始化和针对 Web 的 ApplicationContext。
spring test:主要为测试提供支持的,支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。
Spring 框架中都用到了哪些设计模式?
工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
**单例模式:**Bean默认为单例模式。
代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
详细讲解一下核心容器(spring context应用上下文) 模块
这是基本的Spring模块,提供spring 框架的基础功能,BeanFactory 是 任何以spring为基础的应用的核心。Spring 框架建立在此模块之上,它使Spring成为一个容器。
Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从真正的应用代码中分离。最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ,它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。
Spring框架中有哪些不同类型的事件
Spring 提供了以下5种标准的事件:
上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
Spring 应用程序有哪些不同组件?
Spring 应用一般有以下组件:
接口 - 定义功能。
Bean 类 - 它包含属性,setter 和 getter 方法,函数等。
Bean 配置文件 - 包含类的信息以及如何配置它们。
Spring 面向切面编程(AOP) - 提供面向切面编程的功能。
用户程序 - 它使用接口。
使用 Spring 有以下方式:
作为一个成熟的 Spring Web 应用程序。
作为第三方 Web 框架,使用 Spring Frameworks 中间层。
作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects)。
用于远程使用。
Spring面向切面编程(AOP)
什么是AOP
OOP(Object-Oriented Programming)面向对象编程,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
AOP(Aspect-Oriented Programming),一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
Spring AOP and AspectJ AOP 的区别?
AOP 有哪些实现方式?
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理
JDK动态代理和CGLIB动态代理的区别
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理实例; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
如何理解 Spring 中的代理?
将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。
Advice + Target Object = Proxy
解释一下Spring AOP里面的几个名词
(1)切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容。 在Spring AOP中,切面可以使用通用类(基于模式的风格) 或者在普通类中以 @AspectJ 注解来实现。
(2)连接点(Join point):指方法,在Spring AOP中,一个连接点 总是 代表一个方法的执行。 应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
(3)通知(Advice):在AOP术语中,切面的工作被称为通知。
(4)切入点(Pointcut):切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
(5)引入(Introduction):引入允许我们向现有类添加新方法或属性。
(6)目标对象(Target Object): 被一个或者多个切面(aspect)所通知(advise)的对象。它通常是一个代理对象。也有人把它叫做 被通知(adviced) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。
(7)织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多少个点可以进行织入:
编译期:切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的。
类加载期:切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。
运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。
Spring在运行时通知对象
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。代理封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入SpringAOP的切面。
Spring只支持方法级别的连接点
因为Spring基于动态代理,所以Spring只支持方法连接点。Spring缺少对字段连接点的支持,而且它不支持构造器连接点。方法之外的连接点拦截功能,我们可以利用Aspect来补充。
在Spring AOP 中,关注点和横切关注的区别是什么?在 spring aop 中 concern 和 cross-cutting concern 的不同之处
关注点(concern)是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。
横切关注点(cross-cutting concern)是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输,几乎应用的每个模块都需要的功能。因此这些都属于横切关注点。
Spring通知有哪些类型?
在AOP术语中,切面的工作被称为通知,实际上是程序执行时要通过SpringAOP框架触发的代码段。
Spring切面可以应用5种类型的通知:
前置通知(Before):在目标方法被调用之前调用通知功能;
后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
返回通知(After-returning ):在目标方法成功执行之后调用通知;
异常通知(After-throwing):在目标方法抛出异常后调用通知;
环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。同一个aspect,不同advice的执行顺序:
①没有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterReturning
②有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterThrowing:异常发生
java.lang.RuntimeException: 异常发生
什么是切面 Aspect?
aspect 由 pointcount 和 advice 组成,切面是通知和切点的结合。 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑编织到切面所指定的连接点中.
AOP 的工作重心在于如何将增强编织目标对象的连接点上, 这里包含两个工作:
如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
如何在 advice 中编写切面代码.
可以简单地认为, 使用 @Aspect 注解的类就是切面.
Spring控制反转(IOC)
什么是Spring IOC 容器?
控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
控制反转(IoC)有什么作用
管理对象的创建和依赖关系的维护。对象的创建并不是一件简单的事,在对象关系比较复杂时,如果依赖关系需要程序猿来维护的话,那是相当头疼的
解耦,由容器去维护具体的对象
托管了类的产生过程,比如我们需要在类的产生过程中做一些处理,最直接的例子就是代理,如果有容器程序可以把这部分处理交给容器,应用程序则无需去关心类是如何完成代理的
IOC的优点是什么?
IOC 或 依赖注入把应用的代码量降到最低。
它使应用容易测试,单元测试不再需要单例和JNDI查找机制。
最小的代价和最小的侵入性使松散耦合得以实现。
IOC容器支持加载服务时的饿汉式初始化和懒加载。
Spring IoC 的实现机制
Spring 中的 IoC 的实现原理就是工厂模式加反射机制。
interface Fruit {
public abstract void eat();
}
class Apple implements Fruit {
public void eat(){
System.out.println("Apple");
}
}
class Orange implements Fruit {
public void eat(){
System.out.println("Orange");
}
}
class Factory {
public static Fruit getInstance(String ClassName) {
Fruit f=null;
try {
f=(Fruit)Class.forName(ClassName).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
class Client {
public static void main(String[] a) {
Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple");
if(f!=null){
f.eat();
}
}
}
Spring 的 IoC支持哪些功能
Spring 的 IoC 设计支持以下功能:
依赖注入
依赖检查
自动装配
支持集合
指定初始化方法和销毁方法
支持回调某些方法(但是需要实现 Spring 接口,略有侵入)
其中,最重要的就是依赖注入,从 XML 的配置上说,即 ref 标签。对应 Spring RuntimeBeanReference 对象。
对于 IoC 来说,最重要的就是容器。容器管理着 Bean 的生命周期,控制着 Bean 的依赖注入。
BeanFactory 和 ApplicationContext有什么区别?
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
依赖关系
BeanFactory:是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。
ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
继承MessageSource,因此支持国际化。
统一的资源文件访问方式。
提供在监听器中注册bean的事件。
同时加载多个配置文件。
载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
加载方式
BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。
相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
创建方式
BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
注册方式
BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
Spring 如何设计容器的,BeanFactory和ApplicationContext的关系详解
Spring 作者 Rod Johnson 设计了两个接口用以表示容器。
BeanFactory
ApplicationContext
BeanFactory 简单粗暴,可以理解为就是个 HashMap,Key 是 BeanName,Value 是 Bean 实例。通常只提供注册(put),获取(get)这两个功能。我们可以称之为 “低级容器”。
ApplicationContext 可以称之为 “高级容器”。因为他比 BeanFactory 多了更多的功能。他继承了多个接口。因此具备了更多的功能。例如资源的获取,支持多种消息(例如 JSP tag 的支持),对 BeanFactory 多了工具级别的支持等待。所以你看他的名字,已经不是 BeanFactory 之类的工厂了,而是 “应用上下文”, 代表着整个大容器的所有功能。该接口定义了一个 refresh 方法,此方法是所有阅读 Spring 源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的 bean。
当然,除了这两个大接口,还有其他的辅助接口,这里就不介绍他们了。
BeanFactory和ApplicationContext的关系
为了更直观的展示 “低级容器” 和 “高级容器” 的关系,这里通过常用的 ClassPathXmlApplicationContext 类来展示整个容器的层级 UML 关系。
有点复杂? 先不要慌,我来解释一下。
最上面的是 BeanFactory,下面的 3 个绿色的,都是功能扩展接口,这里就不展开讲。
看下面的隶属 ApplicationContext 粉红色的 “高级容器”,依赖着 “低级容器”,这里说的是依赖,不是继承哦。他依赖着 “低级容器” 的 getBean 功能。而高级容器有更多的功能:支持不同的信息源头,可以访问文件资源,支持应用事件(Observer 模式)。
通常用户看到的就是 “高级容器”。 但 BeanFactory 也非常够用啦!
左边灰色区域的是 “低级容器”, 只负载加载 Bean,获取 Bean。容器其他的高级功能是没有的。例如上图画的 refresh 刷新 Bean 工厂所有配置,生命周期事件回调等。
小结
说了这么多,不知道你有没有理解Spring IoC? 这里小结一下:IoC 在 Spring 里,只需要低级容器就可以实现,2 个步骤:
加载配置文件,解析成 BeanDefinition 放在 Map 里。
调用 getBean 的时候,从 BeanDefinition 所属的 Map 里,拿出 Class 对象进行实例化,同时,如果有依赖关系,将递归调用 getBean 方法 —— 完成依赖注入。
上面就是 Spring 低级容器(BeanFactory)的 IoC。
至于高级容器 ApplicationContext,他包含了低级容器的功能,当他执行 refresh 模板方法的时候,将刷新整个容器的 Bean。同时其作为高级容器,包含了太多的功能。一句话,他不仅仅是 IoC。他支持不同信息源头,支持 BeanFactory 工具类,支持层级容器,支持访问文件资源,支持事件发布通知,支持接口回调等等。
ApplicationContext通常的实现是什么?
FileSystemXmlApplicationContext :此容器从一个XML文件中加载beans的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
ClassPathXmlApplicationContext:此容器也从一个XML文件中加载beans的定义,这里,你需要正确设置classpath因为这个容器将在classpath里找bean配置。
WebXmlApplicationContext:此容器加载一个XML文件,此文件定义了一个WEB应用的所有bean。
什么是Spring的依赖注入?
控制反转IoC是一个很大的概念,可以用不同的方式来实现。其主要实现方式有两种:依赖注入和依赖查找
依赖注入:相对于IoC而言,依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入(Dependency Injection),即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。
依赖注入的基本原则
依赖注入的基本原则是:应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责,“查找资源”的逻辑应该从应用组件的代码中抽取出来,交给IoC容器负责。容器全权负责组件的装配,它会把符合依赖关系的对象通过属性(JavaBean中的setter)或者是构造器传递给需要的对象。
依赖注入有什么优势
依赖注入之所以更流行是因为它是一种更可取的方式:让容器全权负责依赖查询,受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口,使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相比,主要优势为:
查找定位操作与应用代码完全无关。
不依赖于容器的API,可以很容易地在任何容器以外使用应用对象。
不需要特殊的接口,绝大多数对象可以做到完全不必依赖容器。
有哪些不同类型的依赖注入实现方式?
依赖注入是时下最流行的IoC实现方式,依赖注入分为接口注入(Interface Injection),Setter方法注入(Setter Injection)和构造器注入(Constructor Injection)三种方式。其中接口注入由于在灵活性和易用性比较差,现在从Spring4开始已被废弃。
构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。
Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。
构造器依赖注入和 Setter方法注入的区别
两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。
Spring事务的实现方式和实现原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
什么是事务
数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起成功,要么一起失败,是一个不可分割的工作单元。
在我们日常工作中,涉及到事务的场景非常多,一个 service 中往往需要调用不同的 dao 层方法,这些方法要么同时成功要么同时失败,我们需要在 service 层确保这一点
事务的四大特性:A:原子性 C:一致性 I:隔离性 D:持久性
Spring支持的事务管理类型, spring 事务实现方式有哪些?
Spring支持两种类型的事务管理:
编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。
声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。
Spring中事务的实现方式
1、编程式—实现事务
在applicationContext.xml中配置好数据源,和事务管理器:
以上这种方式 不推荐使用,代码入侵太多。大量的处理事务的代码穿插到业务代码中
2、声明式—实现事务
(1)、声明式事务:xml形式 提前配置好数据源
- 配置事务管理器
- 配置通知,添加事务的切面
- Aop的织入,将切面和切入点绑定起来
(2)、configration配置类的形式配置声明式事务
1、配置好数据源信息 2、配置事务管理器 3、开启事务的注解支持
将该配置类添加到包扫描路径下,接来下就可以直接在service的方法或者类上使用@Transactional注解给方法添加事务
(3)、xml+注解方式配置声明式事务
配置完成后,只需要在想要开启注解的方法上加上@Transactional注解就可以了
说一下Spring的事务传播行为
spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。
① PROPAGATION_REQUIRED:默认的事务传播,如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
在一个事务执行的过程中,调用另一个事务时候(比如一个service方法调用另一个service方法),这个事务将以何种状态存在,是两个事务共存呢,还是一个事务是另一个事务的子事务,还是一个事务加入另一个事务的子事务呢……利用事务的传播性来解决这个问题。
1、REQUIRED: spring默认的事务的传播性
REQUIRED 表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
@Service
public class AccountService {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional
public void handle1() {
jdbcTemplate.update("update user set money = ? where id=?;", 1, 2);
}
}
@Service
public class AccountService2 {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
AccountService accountService;
public void handle2() {
jdbcTemplate.update("update user set money = ? where username=?;", 1, "zhangsan");
accountService.handle1();
}
}
如果 handle2 方法本身是有事务的,则 handle1 方法就会加入到 handle2 方法所在的事务中,这样两个方法将处于同一个事务中,一起成功或者一起失败(不管是 handle2 还是 handle1 谁抛异常,都会导致整体回滚)。
如果 handle2 方法本身是没有事务的,则 handle1 方法就会自己开启一个新的事务。
2、REQUIRES_NEW
REQUIRES_NEW 表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。换言之,不管外部方法是否有事务,REQUIRES_NEW 都会开启自己的事务。
3、NESTED
NESTED 表示如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
4、MANDATORY
MANDATORY 表示如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
5、SUPPORTS
NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。
6、NOT_SUPPORTED
NOT_SUPPORTED 表示以非事务方式运行,如果当前存在事务,则把当前事务挂起。
7、NEVER
NEVER 表示以非事务方式运行,如果当前存在事务,则抛出异常。
spring事务的实现原理
底层是通过aop进行实现,@Transactional注解使用环绕通知,在进入方法前开启事务 。使用try catch包含目标方法,执行目标方法,执行完成后如果没有抛出异常,就提交事务。如果抛出异常就进行回滚。
代码实现:
定义注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface rkTransactional {
}
切面:
@Aspect
@Component
@Slf4j
public class ExtrkThreadAop {
@Autowired
private RkTransaction rkTransaction;
/**
* 只要方法上有加上rkTransactional 走around()
* 异常通知
* @param joinPoint
* @throws Throwable
*/
@Around(value = "@annotation(com.rk.aop.rkTransactional)")
public Object around(ProceedingJoinPoint joinPoint) {
// 在目标方法之前开启事务 底层实现:将事务状态保存在当前线程里面
TransactionStatus transactionStatus = rkTransaction.begin();
try {
Object result = joinPoint.proceed();//目标方法
log.info("目标方法之后执行");
//提交事务
rkTransaction.commit(transactionStatus);
return result;
} catch (Throwable throwable) {
// 目标方法执行向外抛出异常之后 手动回滚
rkTransaction.rollback(transactionStatus);
return "fail";
}
}
}
注解类:
@Component
public class RkTransaction {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
// 开启事务
public TransactionStatus begin() {
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
return transaction;
}
// 提交事务
public void commit(TransactionStatus transactionStatus) {
dataSourceTransactionManager.commit(transactionStatus);
}
// 回滚事务
public void rollback(TransactionStatus transactionStatus) {
dataSourceTransactionManager.rollback(transactionStatus);
}
}
test: 测试
/**
* 使用事务注解 事务到底在什么时候提交呢?该方法没有抛出异常的情况下就会自动提交事务
* aop
* @param name
* @return
*/
@GetMapping("/insertUser")
@rkTransactional
public String insertUser(String name) {
int result = userMapper.insertUser(name);
if ("rk".equals(name)) {
int j = 1 / 0;
}
return result > 0 ? "ok" : "fail";
}
}
Spring 事务失效的7种场景
- 未启用spring事务管理功能
- 方法不是public类型的
- 数据源未配置事务管理器
- 自身调用问题
- 异常类型错误
- 异常被吞了
- 业务和spring事务代码必须在一个线程中
1、数据库不支持事务
2、没有配置事务管理器
3、事务所在的方法没有被public修饰
4、异常被catch,没有抛出,事务会失效
5、异常类型错误,默认是runtimeException才会回滚的
解决方案:加上@Transactional(rollbackFor = Exception.class)注解;这样Exception也会回滚
6、用final或者static关键字修饰的方法事务会失效
7、事务需要从外部调用,Spring 自调事务用会失效。即相同类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效,这点尤其要注意,因为代理模式只拦截通过代理传入的外部方法调用,所以自调用事务是不生效的。
1.1、未启用spring事务管理功能
@EnableTransactionManagement 注解用来启用spring事务自动管理事务的功能,这个注解千万不要忘记写了。
1.2、方法不是public类型的
@Transaction 可以用在类上、接口上、public方法上,如果将@Trasaction用在了非public方法上,事务将无效。
1.3、数据源未配置事务管理器
spring是通过事务管理器了来管理事务的,一定不要忘记配置事务管理器了,要注意为每个数据源配置一个事务管理器:
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
1.4、自身调用问题
spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。
看下面代码,大家思考一个问题:当外部直接调用m1的时候,m2方法的事务会生效么?
@Component
public class UserService {
public void m1(){
this.m2();
}
@Transactional
public void m2(){
//执行db操作
}
}
显然不会生效,因为m1中通过this的方式调用了m2方法,而this并不是代理对象,this.m2()不会被事务拦截器,所以事务是无效的,如果外部直接调用通过UserService这个bean来调用m2方法,事务是有效的,上面代码可以做一下调整,如下,@1在UserService中注入了自己,此时m1中的m2事务是生效的.
@Component
public class UserService {
@Autowired //@1
private UserService userService;
public void m1() {
this.userService.m2();
}
@Transactional
public void m2() {
//执行db操作
}
}
重点:必须通过代理对象访问方法,事务才会生效。
1.5、异常类型错误
spring事务回滚的机制:对业务方法进行try catch,当捕获到有指定的异常时,spring自动对事务进行回滚,那么问题来了,哪些异常spring会回滚事务呢?
并不是任何异常情况下,spring都会回滚事务,默认情况下,RuntimeException和Error的情况下,spring事务才会回滚。
也可以自定义回滚的异常类型:
@Transactional(rollbackFor = {异常类型列表})
1.6、异常被吞了
当业务方法抛出异常,spring感知到异常的时候,才会做事务回滚的操作,若方法内部将异常给吞了,那么事务无法感知到异常了,事务就不会回滚了。
如下代码,事务操作2发生了异常,但是被捕获了,此时事务并不会被回滚
@Transactional
public void m1(){
//事务操作1
try{
//事务操作2,内部抛出了异常
}catch(Exception e){
}
}
1.7、业务和spring事务代码必须在一个线程中
spring事务实现中使用了ThreadLocal,ThreadLocal大家应该知道吧,可以实现同一个线程中数据共享,必须是同一个线程的时候,数据才可以共享,这就要求业务代码必须和spring事务的源码执行过程必须在一个线程中,才会受spring事务的控制,比如下面代码,方法内部的子线程内部执行的事务操作将不受m1方法上spring事务的控制,这个大家一定要注意
@Transactional
public void m1() {
new Thread() {
//一系列事务操作
}.start();
}
2、如何快速定位事务相关bug?
2种方式
方式1:看日志
如果你使用了logback或者log4j来输出日志,可以修改一下日志级别为debug模式,可以看到事务的详细执行日志,帮助你定位错误
方式2:调试代码
如果你对源码比较了解,那么你会知道被spring管理事务的业务方法,执行的时候都会被TransactionInterceptor拦截器拦截,会进入到它的invoke方法中,咱们可以在invoke方法中设置一些断点,可以看到详细的执行过程,排错也就比较容易了。
整体上来说,还是需要你深入理解原理,原理了解了,写代码的时候本身就会避免很多坑。
说一下 spring 的事务隔离?
spring 有五大隔离级别,默认值为 ISOLATION_DEFAULT(使用数据库的设置),其他四个隔离级别和数据库的隔离级别一致:
ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
ISOLATION_READ_UNCOMMITTED:读未提交,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
ISOLATION_READ_COMMITTED:读已提交,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server 的默认级别;
ISOLATION_REPEATABLE_READ:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL 的默认级别;
ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
不可重复读 :是指在一个事务内,多次读同一数据。
幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
注意:事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。
Spring框架的事务管理有哪些优点?
为不同的事务API 如 JTA,JDBC,Hibernate,JPA 和JDO,提供一个不变的编程模式。
为编程式事务管理提供了一套简单的API而不是一些复杂的事务API
支持声明式事务管理。
和Spring各种数据访问抽象层很好得集成。
你更倾向用哪种事务管理类型?
大多数Spring框架的用户选择声明式事务管理,因为它对应用代码的影响最小,因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵活性。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
Spring Beans
什么是Spring beans?
Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中 的形式定义。
一个 Spring Bean 定义 包含什么?
一个Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。
如何给Spring 容器提供配置元数据?Spring有几种配置方式
这里有三种重要的方法给Spring 容器提供配置元数据。
XML配置文件。
基于注解的配置。
基于java的配置。
Spring配置文件包含了哪些信息
Spring配置文件是个XML 文件,这个文件包含了类信息,描述了如何配置它们,以及如何相互调用。
Spring基于xml注入bean的几种方式
Set方法注入;
构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
静态工厂注入;
实例工厂;
你怎样定义类的作用域?
当定义一个 在Spring里,我们还能给这个bean声明一个作用域。它可以通过bean 定义中的scope属性来定义。如,当Spring要在需要的时候每次生产一个新的bean实例,bean的scope属性被指定为prototype。另一方面,一个bean每次使用的时候必须返回同一个实例,这个bean的scope 属性 必须设为 singleton。
解释Spring支持的几种bean的作用域
Spring框架支持以下五种bean的作用域:
singleton : bean在每个Spring ioc 容器中只有一个实例。
prototype:一个bean的定义可以有多个实例。
request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
注意: 缺省的Spring bean 的作用域是Singleton。使用 prototype 作用域需要慎重的思考,因为频繁创建和销毁 bean 会带来很大的性能开销。
Spring框架中的单例bean是线程安全的吗?
不是,Spring框架中的单例bean不是线程安全的。
spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。
实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。
有状态就是有数据存储功能。
无状态就是不会保存数据。
Spring如何处理线程并发问题?
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
请解释Spring Bean的生命周期
首先说一下Servlet的生命周期:实例化,初始init,接收请求service,销毁destroy;
Spring上下文中的Bean生命周期也类似,如下:
(1)实例化Bean:
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
(3)处理Aware接口:
接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor:
如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(6)如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(7)DisposableBean:
当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
(8)destroy-method:
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
解释Spring支持的几种bean的作用域
Spring容器中的bean可以分为5个范围:
(1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。
(2)prototype:为每一个bean请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。
(5)global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。
8、Spring框架中的单例Beans是线程安全的么?
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”。
Spring如何处理线程并发问题?
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
Spring基于xml注入bean的几种方式:
(1)Set方法注入;
(2)构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
(3)静态工厂注入;
(4)实例工厂;
详细内容可以阅读:https://www.iteye.com/blog/blessht-1162131
Spring的自动装配:
在spring中,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象,使用autowire来配置自动装载模式。
在Spring框架xml配置中共有5种自动装配:
(1)no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
(2)byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
(3)byType:通过参数的数据类型进行自动装配。
(4)constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
(5)autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
基于注解的方式:
使用@Autowired注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置,<context:annotation-config />。在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。在使用@Autowired时,首先在容器中查询对应类型的bean:
如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;
如果查询的结果不止一个,那么@Autowired会根据名称来查找;
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。
@Autowired可用于:构造函数、成员变量、Setter方法
注:@Autowired和@Resource之间的区别
(1) @Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
(2) @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
Spring 框架中都用到了哪些设计模式?
(1)工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
(2)单例模式:Bean默认为单例模式。
(3)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
(4)模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
(5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
spring 是如何开启事务的
核心原理
Spring事务管理的实现有许多细节,如果对整个接口框架有个大体了解会非常有利于我们理解事务,下面通过讲解Spring的事务接口来了解Spring实现事务的具体策略。
Spring事务管理涉及的接口的联系如下:
spring开启事务有两种:
1 基于注解开启事务
只需要在方法头上加一个注解@Transactional即可:
2 基于代码来开启事务
在需要开启事务的方法中,输入以下代码即可:
@Autowired
private PlatformTransactionManager txManager;
// 开启事务管理
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
//操作都放在这里
}catch(Exception e){
//这里是异常处理
}
txManager.commit(status);//事务提交
spring的事务在什么场景下会失效
1:如果数据库不支持事务,则失效
因为事务是作用于数据库。例如使用MySQL且引擎是MyISAM,则事务会不起作用,因为MyISAM引擎本身不支持事务;如果改成InnoDB,则可以。
2:Service类没有被Spring管理
因为Spring的事务是基于AOP,所以如果Service类没有被Spring管理,变成一个Spring Bean,即使添加了**@Transactional**注解,事务也是无效的。
**3:**内部调用
不带事务的方法调用该类中带事务的方法,不会回滚。因为Spring的回滚是用过代理模式生成的,如果是一个不带事务的方法调用该类的带事务的方法,直接通过this.xxx()
调用,而不生成代理事务,所以事务不起作用。常见解决方法“拆类”。
4:使用默认的事务处理方式
Spring的事务默认是对RuntimeException进行回滚,而不继承RuntimeException的不回滚。因为在java的设计中,它认为不继承RuntimeException的异常是CheckException或普通异常,如IOException,这些异常在java语法中是要求强制处理的。对于这些普通异常,Spring默认它们都已经处理,所以默认不回滚。可以添加rollbackfor=Exception.class来表示所有的Exception都回滚。
5:事务只能应用于 public 方法
@Transactional注解只能应用于public方法,如果你在protected、private或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
6数据源没有配置事务管理器
spring 对事务如何进行管理
参考文章:spring对事务的管理
Spring 支持两种方式事务管理
一:编程式的事务管理
通过TransactionTemplate手动管理事务
在实际应用中很少使用,原因是要修改原来的代码,加入事务管理代码 (侵入性 )
二:声明式事务管理(XML配置文件方式或注解方式)
Spring的声明式事务是通过AOP实现的(环绕通知)
开发中经常使用(代码侵入性最小)–推荐使用!
注:后面会演示声明式事务管理的两种方式:xml配置文件方式和注解方式,不演示编程式方式
Spring的事务机制包括声明式事务和编程式事务。
编程式事务管理:Spring推荐使用TransactionTemplate,实际开发中使用声明式事务较多。
声明式事务管理:将我们从复杂的事务处理中解脱出来,获取连接,关闭连接、事务提交、回滚、异常处理等这些操作都不用我们处理了,Spring都会帮我们处理。
声明式事务管理使用了AOP面向切面编程实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
Spring JDBC-Spring对事务管理的支持
参考 Spring对事务管理的支持 此文章。
spring如何对bean进行自动的事务管理
1、Spring事务管理概述
Spring的事务管理简化了传统的事务管理流程,并且在一定程度上减少了开发者的工作量。
1.1 事务管理的核心接口
在Spring的所有JAR包中包含一个名为Spring-tx-4.3.6.RELEASE的JAR包,该包就是Spring提供的用于事务管理的依赖包。在该JAR包的org.Springframework.transaction包中有3个接口文件:PlatformTransactionManager、TransactionDefinition和TransactionStatus。
- PlatformTransactionManager
PlatformTransactionManager接口是Spring提供的平台事务管理器,主要用于管理事务。该接口中提供了3个事务操作的方法,具体如下。
TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。该方法会根据TransactionDefinition参数返回一个TransactionStatus对象。TransactionStatus对象表示一个事务,被关联在当前执行的线程上。
void commit(TransactionStatus status):用于提交事务。
void rollback(TransactionStatus status):用于回滚事务。
PlatformTransactionManager接口只是代表事务管理的接口,并不知道底层是如何管理事务的,它只需要事务管理提供上面的3个方法,但具体如何管理事务则由它的实现类来完成。
PlatformTransactionManager接口有许多不同的实现类,常见的几个实现类如下。
org.springframework.jdbc.datasource.DataSourceTransactionManager:用于配置JDBC数据源的事务管理器。
org.springframework.orm.Hibernate4.HibernateTransactionManager:用于配置Hibernate的事务管理器。
org.springframework.transaction.jta.JtaTransactionManager:用于配置全局事务管理器。
当底层采用不同的持久层技术时,系统只需使用不同的PlatformTransactionManager实现类即可。
2、TransactionDefinition
TransactionDefinition接口是事务定义(描述)的对象,该对象中定义了事务规则,并提供了获取事务相关信息的方法,具体如下:
string getName():获取事务对象名称。
int getlsolationLeve():获取事务的隔离级别。
int getPropagationBehavior():获取事务的传播行为。
int setTimeout():获取事务的超时时间。
boolean isReadOnly():获取事务是否只读。
上述方法中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务。传播行为有很多种,具体如表所示。
在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务。通常情况下,数据的查询不会影响原数据的改变,所以不需要进行事务管理,而对于数据的插入、更新和删除操作,必须进行事务管理。如果没有指定事务的传播行为,Spring默认传播行为是REQUIRED。
3、TransactionStatus
TransactionStatus接口是事务的状态,描述了某一时间点上事务的状态信息。该接口中包含6个方法,具体如下:
void flush():刷新事务。
boolean hasSavepoint():获取是否存在保存点。
boolean isCompleted():获取事务是否完成。
boolean isNewTransaction():获取是否是新事务。
boolean isRollbackOnly():获取是否回滚。
void setRollbackOnly():设置事务回滚。
1.2 事务管理的方式
Spring中的事务管理分为两种方式:一种是传统的编程序事务管理;另一种是声明式事务管理。
编程序事务管理:通过编写代码实现的事务管理,包括定义事务的开始、正常执行后的事务提交和异常时的事务回滚。
声明式事务管理:通过AOP技术实现的事务管理,其主要思想是将事务管理作为一个“切面”代码单独编写,然后通过AOP技术将事务管理的“切面”代码植入业务目标类中。
声明式事务管理最大的优点在于开发者无须通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务规则应用到业务逻辑中。这使得开发人员可以更加专注于核心业务逻辑代码的编写,在一定程度上减少了工作量,提高了开发效率。所以在实际开发中,通常都推荐使用声明式事务管理。
4、声明式事务管理
Spring的声明式事务管理可以通过两种方式来实现:一种是基于XML的方式;另一种是基于Annotation的方式。
2.1 基于XML方式的声明式事务
基于XML方式的声明式事务管理是通过在配置文件中配置事务规则的相关声明来实现的。
Spring 2.0以后,提供了tx命名空间来配置事务,tx命名空间下提供了tx:advice元素来配置事务的通知(增强处理)。当使用tx:advice元素配置了事务的增强处理后,就可以通过编写的AOP配置让Spring自动对目标生成代理。配置tx:advice元素时,通常需要指定id和transaction-manager属性,其中id属性是配置文件中的唯一标识,transaction-manager属性用于指定事务管理器。除此之外,还需要配置一个tx:attributes子元素,该子元素可通过配置多个tx:method子元素来配置执行事务的细节。
关于tx:method元素的属性描述如表所示。
Spring事务的实现方式和实现原理:
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
(1)Spring事务的种类:
spring支持编程式事务管理和声明式事务管理两种方式:
①编程式事务管理使用TransactionTemplate。
②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。
声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
(2)spring的事务传播行为:
spring事务的传播行为说的是,当多个事务同时存在的时候,spring是如何处理这些事务的行为。
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
(3)Spring中的隔离级别:
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新。
④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新。
⑤ ISOLATION_SERIALIZABLE:可串行化,一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。
(4)spring中事务的配置方式
-
配置文件配置方式:
<tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <!--设置所有匹配的方法,然后设置传播级别和事务隔离--> <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="create*" propagation="REQUIRED" /> <tx:method name="insert*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="merge*" propagation="REQUIRED" /> <tx:method name="del*" propagation="REQUIRED" /> <tx:method name="remove*" propagation="REQUIRED" /> <tx:method name="put*" propagation="REQUIRED" /> <tx:method name="get*" propagation="SUPPORTS" read-only="true" /> <tx:method name="count*" propagation="SUPPORTS" read-only="true" /> <tx:method name="find*" propagation="SUPPORTS" read-only="true" /> <tx:method name="list*" propagation="SUPPORTS" read-only="true" /> <tx:method name="*" propagation="SUPPORTS" read-only="true" /> </tx:attributes> </tx:advice>
-
注解的方式:
<!--开启注解的方式--> <tx:annotation-driven transaction-manager="transactioManager" />
@Transactional(propagation=Propagation.REQUIRED)
如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED)
容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY)
必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER)
必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS)
如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
(5) Spring框架中有哪些不同类型的事件?
Spring 提供了以下5种标准的事件:
(1)上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
(2)上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
(3)上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
(4)上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
(5)请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。
如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
五、springBoot
springBoot的实现原理
参考的url
什么是 Spring Boot?
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
SpringBoot是什么
springboot是依赖于spring的,比起spring,除了拥有spring的全部功能以外,springboot无需繁琐的xml配置,这取决于它自身强大的自动装配功能;并且自身已嵌入Tomcat、Jetty等web容器,集成了springmvc,使得springboot可以直接运行,不需要额外的容器,提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等,
其实spring大家都知道,boot是启动的意思。所以,spring boot其实就是一个启动spring项目的一个工具而已,总而言之,springboot 是一个服务于框架的框架;也可以说springboot是一个工具,这个工具简化了spring的配置;
Spring Boot的核心功能
1、 可独立运行的Spring项目:Spring Boot可以以jar包的形式独立运行。
2、 内嵌的Servlet容器:Spring Boot可以选择内嵌Tomcat、Jetty或者Undertow,无须以war包形式部署项目。
3、 简化的Maven配置:Spring提供推荐的基础 POM 文件来简化Maven 配置。
4、 自动配置Spring:Spring Boot会根据项目依赖来自动配置Spring 框架,极大地减少项目要使用的配置。
5、 提供生产就绪型功能:提供可以直接在生产环境中使用的功能,如性能指标、应用信息和应用健康检查。
6、 无代码生成和xml配置:Spring Boot不生成代码。完全不需要任何xml配置即可实现Spring的所有配置。
Spring Boot 主要有如下优点:
容易上手,提升开发效率,为 Spring 开发提供一个更快、更广泛的入门体验。
开箱即用,远离繁琐的配置。
提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。
没有代码生成,也不需要XML配置。
避免大量的 Maven 导入和各种版本冲突。
SpringBoot启动过程-流程
springboot的启动经过了一些一系列的处理,我们先看看整体过程的流程图:
一、SpringBoot启动的时候,会构造一个SpringApplication的实例,构造SpringApplication的时候会进行初始化的工作,初始化的时候会做以下几件事:
1、把参数sources设置到SpringApplication属性中,这个sources可以是任何类型的参数.
2、判断是否是web程序,并设置到webEnvironment的boolean属性中.
3、创建并初始化ApplicationInitializer,设置到initializers属性中 。
4、创建并初始化ApplicationListener,设置到listeners属性中 。
5、初始化主类mainApplicatioClass。
二、SpringApplication构造完成之后调用run方法,启动SpringApplication,run方法执行的时候会做以下几件事:
1、构造一个StopWatch计时器,用来记录SpringBoot的启动时间 。
2、初始化监听器,获取SpringApplicationRunListeners并启动监听,用于监听run方法的执行。
3、创建并初始化ApplicationArguments,获取run方法传递的args参数。
4、创建并初始化ConfigurableEnvironment(环境配置)。封装main方法的参数,初始化参数,写入到 Environment中,发布 ApplicationEnvironmentPreparedEvent(环境事件),做一些绑定后返回Environment。
5、打印banner和版本。
6、构造Spring容器(ApplicationContext)上下文。先填充Environment环境和设置的参数,如果application有设置beanNameGenerator(bean)、resourceLoader(加载器)就将其注入到上下文中。调用初始化的切面,发布ApplicationContextInitializedEvent(上下文初始化)事件。
7、SpringApplicationRunListeners发布finish事件。
8、StopWatch计时器停止计时,日志打印总共启动的时间。
9、发布SpringBoot程序已启动事件(started())
10、调用ApplicationRunner和CommandLineRunner
11、最后发布就绪事件ApplicationReadyEvent,标志着SpringBoot可以处理就收的请求了(running())
Spring Boot 的核心注解是哪个?
由以下注解组成
启动类上面的注解是**@SpringBootApplication**,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
配置
什么是 JavaConfig?
Spring JavaConfig 是 Spring 社区的产品,它提供了配置 Spring IoC 容器的纯Java 方法。因此它有助于避免使用 XML 配置。使用 JavaConfig 的优点在于:
(1)面向对象的配置。由于配置被定义为 JavaConfig 中的类,因此用户可以充分利用 Java 中的面向对象功能。一个配置类可以继承另一个,重写它的**@Bean** 方法等。
(2)减少或消除 XML 配置。基于依赖注入原则的外化配置的好处已被证明。但是,许多开发人员不希望在 XML 和 Java 之间来回切换。JavaConfig 为开发人员提供了一种纯 Java 方法来配置与 XML 配置概念相似的 Spring 容器。从技术角度来讲,只使用 JavaConfig 配置类来配置容器是可行的,但实际上很多人认为将JavaConfig 与 XML 混合匹配是理想的。
(3)类型安全和重构友好。JavaConfig 提供了一种类型安全的方法来配置 Spring容器。由于 Java 5.0 对泛型的支持,现在可以按类型而不是按名称检索 bean,不需要任何强制转换或基于字符串的查找。
Spring Boot 自动配置原理是什么?
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,
@EnableAutoConfiguration 给容器导入META-INF/spring.factories 里定义的自动配置类。
筛选有效的自动配置类。
每一个自动配置类结合对应的 xxxProperties.java 读取配置文件进行自动配置功能
你如何理解 Spring Boot 配置加载顺序?
在 Spring Boot 里面,可以使用以下几种方式来加载配置。
1)properties文件;
2)YAML文件;
3)系统环境变量;
4)命令行参数;
等等……
什么是 YAML?
YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构化,而且更少混淆。可以看出 YAML 具有分层配置数据。
YAML 配置的优势在哪里 ?
YAML 现在可以算是非常流行的一种配置文件格式了,无论是前端还是后端,都可以见到 YAML 配置。那么 YAML 配置和传统的 properties 配置相比到底有哪些优势呢?
配置有序,在一些特殊的场景下,配置有序很关键
支持数组,数组中的元素可以是基本数据类型也可以是对象
简洁
相比 properties 配置文件,YAML 还有一个缺点,就是不支持 @PropertySource 注解导入自定义的 YAML 配置。
Spring Boot 是否可以使用 XML 配置 ?
Spring Boot 推荐使用 Java 配置而非 XML 配置,但是 Spring Boot 中也可以使用 XML 配置,通过 @ImportResource 注解可以引入一个 XML 配置。
spring boot 核心配置文件是什么? bootstrap.properties 和 application.properties 有何区别 ?
单纯做 Spring Boot 开发,可能不太容易遇到 bootstrap.properties 配置文件,但是在结合 Spring Cloud 时,这个配置就会经常遇到了,特别是在需要加载一些远程配置文件的时侯。
spring boot 核心的两个配置文件:
bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。且 boostrap 里面的属性不能被覆盖;
application (. yml 或者 . properties): 由ApplicatonContext 加载,用于 spring boot 项目的自动化配置。
application.yml的优先级高于application.properties 所以优先加载yml的配置文件
什么是 Spring Profiles?
Spring Profiles 允许用户根据配置文件(dev,test,prod 等)来注册 bean。因此,当应用程序在开发中运行时,只有某些 bean 可以加载,而在 PRODUCTION中,某些其他 bean 可以加载。假设我们的要求是 Swagger 文档仅适用于 QA 环境,并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot 使得使用配置文件非常简单。
如何在自定义端口上运行 Spring Boot 应用程序?
为了在自定义端口上运行 Spring Boot 应用程序,您可以在application.properties 中指定端口。server.port = 8090
安全
如何实现 Spring Boot 应用程序的安全性?
为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。
比较一下 Spring Security 和 Shiro 各自的优缺点 ?
由于 Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,包括 Spring Security 的 Starter ,使得在 Spring Boot 中使用 Spring Security 变得更加容易,甚至只需要添加一个依赖就可以保护所有的接口,所以,如果是 Spring Boot 项目,一般选择 Spring Security 。当然这只是一个建议的组合,单纯从技术上来说,无论怎么组合,都是没有问题的。Shiro 和 Spring Security 相比,主要有如下一些特点:
Spring Security 是一个重量级的安全管理框架;Shiro 则是一个轻量级的安全管理框架
Spring Security 概念复杂,配置繁琐;Shiro 概念简单、配置简单
Spring Security 功能强大;Shiro 功能简单
Spring Boot 中如何解决跨域问题 ?
跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他类型的请求,在 RESTful 风格的应用中,就显得非常鸡肋,因此我们推荐在后端通过 (CORS,Cross-origin resource sharing) 来解决跨域问题。这种解决方案并非 Spring Boot 特有的,在传统的 SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
}
Spring Boot 中的监视器是什么?
Spring boot actuator 是 spring 启动框架中的重要功能之一。Spring boot 监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为 HTTP URL 访问的REST 端点来检查状态。
如何在 Spring Boot 中禁用 Actuator 端点安全性?
默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATOR 角色的用户才能访问它们。安全性是使用标准的 HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。
我们如何监视所有 Spring Boot 微服务?
Spring Boot 提供监视器端点以监控各个微服务的度量。这些端点对于获取有关应用程序的信息(如它们是否已启动)以及它们的组件(如数据库等)是否正常运行很有帮助。但是,使用监视器的一个主要缺点或困难是,我们必须单独打开应用程序的知识点以了解其状态或健康状况。想象一下涉及 50 个应用程序的微服务,管理员将不得不击中所有 50 个应用程序的执行终端。为了帮助我们处理这种情况,我们将使用位于的开源项目。 它建立在 Spring Boot Actuator 之上,它提供了一个 Web UI,使我们能够可视化多个应用程序的度量。
什么是 WebSockets?
WebSocket 是一种计算机通信协议,通过单个 TCP 连接提供全双工通信信道。
1、WebSocket 是双向的 -使用 WebSocket 客户端或服务器可以发起消息发送。
2、WebSocket 是全双工的 -客户端和服务器通信是相互独立的。
3、单个 TCP 连接 -初始连接使用 HTTP,然后将此连接升级到基于套接字的连接。然后这个单一连接用于所有未来的通信
4、Light -与 http 相比,WebSocket 消息数据交换要轻得多。
什么是 Spring Data ?
Spring Data 是 Spring 的一个子项目。用于简化数据库访问,支持NoSQL 和 关系数据存储。其主要目标是使数据库的访问变得方便快捷。Spring Data 具有如下特点:
SpringData 项目支持 NoSQL 存储:
MongoDB (文档数据库)
Neo4j(图形数据库)
Redis(键/值存储)
Hbase(列族数据库)
SpringData 项目所支持的关系数据存储技术:
JDBC
JPA
Spring Data Jpa 致力于减少数据访问层 (DAO) 的开发量. 开发者唯一要做的,就是声明持久层的接口,其他都交给 Spring Data JPA 来帮你完成!Spring Data JPA 通过规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。
什么是 Spring Batch?
Spring Boot Batch 提供可重用的函数,这些函数在处理大量记录时非常重要,包括日志/跟踪,事务管理,作业处理统计信息,作业重新启动,跳过和资源管理。它还提供了更先进的技术服务和功能,通过优化和分区技术,可以实现极高批量和高性能批处理作业。简单以及复杂的大批量批处理作业可以高度可扩展的方式利用框架处理重要大量的信息。
什么是 FreeMarker 模板?
FreeMarker 是一个基于 Java 的模板引擎,最初专注于使用 MVC 软件架构进行动态网页生成。使用 Freemarker 的主要优点是表示层和业务层的完全分离。程序员可以处理应用程序代码,而设计人员可以处理 html 页面设计。最后使用freemarker 可以将这些结合起来,给出最终的输出页面。
如何集成 Spring Boot 和 ActiveMQ?
对于集成 Spring Boot 和 ActiveMQ,我们使用依赖关系。 它只需要很少的配置,并且不需要样板代码。
什么是 Apache Kafka?
Apache Kafka 是一个分布式发布 - 订阅消息系统。它是一个可扩展的,容错的发布 - 订阅消息系统,它使我们能够构建分布式应用程序。这是一个 Apache 顶级项目。Kafka 适合离线和在线消息消费。
什么是 Swagger?你用 Spring Boot 实现了它吗?
Swagger 广泛用于可视化 API,使用 Swagger UI 为前端开发人员提供在线沙箱。Swagger 是用于生成 RESTful Web 服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过 Swagger 正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此,Swagger消除了调用服务时的猜测。
前后端分离,如何维护接口文档 ?
前后端分离开发日益流行,大部分情况下,我们都是通过 Spring Boot 做前后端分离开发,前后端分离一定会有接口文档,不然会前后端会深深陷入到扯皮中。一个比较笨的方法就是使用 word 或者 md 来维护接口文档,但是效率太低,接口一变,所有人手上的文档都得变。在 Spring Boot 中,这个问题常见的解决方案是 Swagger ,使用 Swagger 我们可以快速生成一个接口文档网站,接口一旦发生变化,文档就会自动更新,所有开发工程师访问这一个在线网站就可以获取到最新的接口文档,非常方便。
如何重新加载 Spring Boot 上的更改,而无需重新启动服务器?Spring Boot项目如何热部署?
可以使用 DEV 工具来实现。通过这种依赖关系,您可以节省任何更改,嵌入式tomcat 将重新启动。Spring Boot 有一个开发工具(DevTools)模块,它有助于提高开发人员的生产力。Java 开发人员面临的一个主要挑战是将文件更改自动部署到服务器并自动重启服务器。开发人员可以重新加载 Spring Boot 上的更改,而无需重新启动服务器。这将消除每次手动部署更改的需要。Spring Boot 在发布它的第一个版本时没有这个功能。这是开发人员最需要的功能。DevTools 模块完全满足开发人员的需求。该模块将在生产环境中被禁用。它还提供 H2 数据库控制台以更好地测试应用程序。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?
Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 java -jar xxx.jar 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。
Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。
运行 Spring Boot 有哪几种方式?
1)打包用命令或者放到容器中运行
2)用 Maven/ Gradle 插件运行
3)直接执行 main 方法运行
Spring Boot 需要独立的容器运行吗?
可以不需要,内置了 Tomcat/ Jetty 等容器。
开启 Spring Boot 特性有哪几种方式?
1)继承spring-boot-starter-parent项目
2)导入spring-boot-dependencies项目依赖
如何使用 Spring Boot 实现异常处理?
Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个 ControlerAdvice 类,来处理控制器类抛出的所有异常。
如何使用 Spring Boot 实现分页和排序?
使用 Spring Boot 实现分页非常简单。使用 Spring Data-JPA 可以实现将可分页的传递给存储库方法。
微服务中如何实现 session 共享 ?
在微服务中,一个完整的项目被拆分成多个不相同的独立的服务,各个服务独立部署在不同的服务器上,各自的 session 被从物理空间上隔离开了,但是经常,我们需要在不同微服务之间共享 session ,常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。这样就实现了 session 共享,Spring Session 基于 Spring 中的代理过滤器实现,使得 session 的同步操作对开发人员而言是透明的,非常简便。
Spring Boot 中如何实现定时任务 ?
定时任务也是一个常见的需求,Spring Boot 中对于定时任务的支持主要还是来自 Spring 框架。
在 Spring Boot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled 注解,另一个则是使用第三方框架 Quartz。
使用 Spring 中的 @Scheduled 的方式主要通过 @Scheduled 注解来实现。
使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。
六、 springMVC
什么是Spring MVC?
Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把模型-视图-控制器分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。
Spring MVC的优点
(1)可以支持各种视图技术,而不仅仅局限于JSP;
(2)与Spring框架集成(如IoC容器、AOP等);
(3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。
(4) 支持各种请求资源的映射策略。
核心组件
Spring MVC的主要组件?
(1)前端控制器 DispatcherServlet(不需要程序员开发)
作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。
(2)处理器映射器HandlerMapping(不需要程序员开发)
作用:根据请求的URL来查找Handler
(3)处理器适配器HandlerAdapter
注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。
(4)处理器Handler(需要程序员开发)
(5)视图解析器 ViewResolver(不需要程序员开发)
作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view)
(6)视图View(需要程序员开发jsp)
View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等)
什么是DispatcherServlet
Spring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。
什么是Spring MVC框架的控制器?
控制器提供一个访问应用程序的行为,此行为通常通过服务接口实现。控制器解析用户输入并将其转换为一个由视图呈现给用户的模型。Spring用一个非常抽象的方式实现了一个控制层,允许用户创建多种用途的控制器。
Spring MVC的控制器是不是单例模式,如果是,有什么问题,怎么解决?
答:是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写字段。
springMVC工作原理/流程
请描述Spring MVC的工作流程?描述一下 DispatcherServlet 的工作流程?
(1)用户发送请求至前端控制器DispatcherServlet;
(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;
(3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter处理器适配器;
(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);
(6)Handler执行完成返回ModelAndView;
(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
(9)ViewResolver解析后返回具体View;
(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
(11)DispatcherServlet响应用户。
MVC框架
MVC是什么?MVC设计模式的好处有哪些
mvc是一种设计模式(设计模式就是日常开发中编写代码的一种好的方法和经验的总结)。模型(model)-视图(view)-控制器(controller),三层架构的设计模式。用于实现前端页面的展现与后端业务数据处理的分离。
mvc设计模式的好处
1.分层设计,实现了业务系统各个组件之间的解耦,有利于业务系统的可扩展性,可维护性。
2.有利于系统的并行开发,提升开发效率。
注解原理是什么
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
Spring MVC常用的注解有哪些?
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
SpingMvc中的控制器的注解一般用哪个,有没有别的注解可以替代?
答:一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。
@Controller注解的作用
在Spring MVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model 返回给对应的View 进行展示。在Spring MVC 中提供了一个非常简便的定义Controller 的方法,我们无需继承特定的类或实现特定的接口,只需使用**@Controller 标记一个类是Controller ,然后使用@RequestMapping 和@RequestParam** 等一些注解用以定义URL 请求和Controller 方法之间的映射,这样的Controller 就能被外界访问到。此外Controller 不会直接依赖于HttpServletRequest 和HttpServletResponse 等HttpServlet 对象,它们可以通过Controller 的方法参数灵活的获取到。
@Controller 用于标记在一个类上,使用它标记的类就是一个Spring MVC Controller 对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器。单单使用@Controller 标记在一个类上还不能真正意义上的说它就是Spring MVC 的一个控制器类,因为这个时候Spring 还不认识它。那么要如何做Spring 才能认识它呢?这个时候就需要我们把这个控制器类交给Spring 来管理。有两种方式:
在Spring MVC 的配置文件中定义MyController 的bean 对象。
在Spring MVC 的配置文件中告诉Spring 该到哪里去找标记为@Controller 的Controller 控制器。
@RequestMapping注解的作用
RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestMapping注解有六个属性,下面我们把她分成三类进行说明(下面有相应示例)。
value, method
value: 指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明);
method: 指定请求的method类型, GET、POST、PUT、DELETE等;
consumes,produces
consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;
params,headers
params: 指定request中必须包含某些参数值是,才让该方法处理。
headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求。
@ResponseBody注解的作用
作用: 该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。
使用时机:返回的数据不是html标签的页面,而是其他某种格式的数据时(如json、xml等)使用;
@PathVariable和@RequestParam的区别
请求路径上有个id的变量值,可以通过@PathVariable来获取 @RequestMapping(value = “/page/{id}”, method = RequestMethod.GET)
@RequestParam用来获得静态的URL请求入参 spring注解时action里用到。
七、 Spring Cloud — 注册中心,配置中心,原理详解
谈谈你对 spring Cloud 的理解
spring Cloud 是一套分布式微服务的技术解决方案它提供了快速构建分布式系统常用的一些组件,比如说配置管理、服务的注册与发现、服务调用的负载均衡、资源隔离、熔断降级等等。
不过 Spring Cloud 只是 Spring 官方提供的一套微服务标准定义;
而真正的实现目前有两套体系用的比较多一个是 Spring Cloud Netflix 另一个是 Spring Cloud Alibaba
Spring Cloud Netflix 是基于 Netflix 这个公司的开源组件集成的一套微服务解决方案,其中的组件有:
**1. Eureka——服务注册与发现 **
2. Feign——服务调用;
**3. Ribbon——负载均衡 **
4. Hystrix——服务熔断
5. Zuul——网关;
Spring Cloud Alibaba 是基于阿里巴巴开源组件集成的一套微服务解决方案,其中包括:
- Dubbo——消息通讯
- Nacos——服务注册与发现
- Seata——事务隔离
- Sentinel——熔断降级;
有了 Spring Cloud 这样的技术生态使得我们在落地微服务架构时,不用去考虑第三方技术集成带来额外成本,只要通过配置组件来完成架构下的技术问题即可,从而,可以让我们更加侧重性能方面 ,以上这些就是我个人对 Spring Cloud 的理解!
注册中心
参考地址: 微服务(二)——注册中心(Eureka、Nacos)_微服务注册中心_364.99°的博客-CSDN博客
需求:当一个服务提供者 Service 部署了多个实例交给 User 远程调用时:
- 服务消费者 User 应该调用哪个实例,如何获取其对应地址和端口?
- User 如何获知实例是否健康?
注册中心作用:
- 帮助管理服务,并帮助服务调用者选择并调用服务
- 实时监测服务实例是否健康
Eureka:
构成:
eureka-server:服务端,注册中心
记录服务信息,心跳监控
eureka-client:客户端,
服务提供者(注册到服务端,定期向服务端发送心跳)、服务消费者(从服务端拉取服务列表,基于负载均衡选择服务)
作用:
服务注册: Service实例启动后,会将自己的信息注册到 eureka服务端
服务拉取: User 根据 实例名 获取 Service 地址列表
服务搭建
配置文件:
我们需要将 eureka 注册到spring容器中,所以需要在配置文件中做相关配置。
server:
port: 8099
spring:
application:
name: eureka_server
eureka:
client:
# 配置eureka服务地址
service-url:
defaultZone: http://127.0.0.1:8099/eureka
项目启动:
为了让项目能启动 eureka,需要在启动类上加一个注解:@EnableEurekaServer
@SpringBootApplication
@EnableEurekaServer
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
启动项目,访问地址:http://127.0.0.1:8099:
注册的服务名称就是配置文件中的名称的大写。
服务注册
在上一步,已经将 eureka-server (eureka服务中心)搭建完毕,现在就开始注册服务实例了。
注意:
无论是 服务提供者 还是 服务消费者,他们的身份都是 eureka-client
记得添加 spring-boot-starter-web 的依赖,不然会报错:Field optionalArgs in org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration$RefreshableEurekaClientConfiguration required a bean of type ‘com.netflix.discovery.AbstractDiscoveryClientOptionalArgs’ that could not be found.
服务发现、服务注册统一都封装在eureka-client依赖。
需要在配置文件中配置 eureka-server 的地址。
spring:
application:
name: service_provider
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8099/eureka
启动多个实例
为了让项目能启动 eureka,需要在启动类上加一个注解:@EnableEurekaClient。
@SpringBootApplication
@EnableEurekaClient
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
我们可以通过 IDEA 自带功能模仿启动多个服务实例。
- 打开 Service 面板
****
复制原来的 provider 启动配置
查看 eureka注册中心
服务发现
依赖导入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件:
spring:
application:
name: service_user
server:
port: 8084
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8099/eureka
服务拉取和负载均衡
服务拉取:
-
修改 controller 代码,将 url 路径的 ip、端口 修改为 服务名:
@RestController @RequestMapping("user") public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping("/{id}") public Book getBookById(@PathVariable("id") Integer id) { // String url = "http://127.0.0.1:8081/provider" + id; String url = "http://service_provider/provider"; if (id != null) { url = url + id; } Book book = restTemplate.getForObject(url, Book.class); return book; } }
注册 RestTemplate 的时候加上注解 @LoadBalanced
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
接口调用:
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{id}")
public Book getBookById(@PathVariable("id") Integer id) {
// String url = "http://127.0.0.1:8081/provider" + id;
String url = "http://PROVIDER/pro/";
if (id != null) {
url = url + id;
}
Book book = restTemplate.getForObject(url, Book.class);
return book;
}
}
访问接口:
访问报错:
解决方案:
多测试几下接口,可以发现,user一会儿调用的是 provider:8081 一会儿调用的是 provider:8082。这就是负载均衡算法选择的。
小结
eureka-server 搭建:
- 引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 启动类添加 @EnableEurekaServer
- 配置文件配置 eureka 地址
server:
port: 8099
spring:
application:
name: server
eureka:
client:
# 配置eureka服务地址
service-url:
defaultZone: http://127.0.0.1:8099/eureka
服务注册:
- 引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 启动类添加 @EnableEurekaClient
- 配置文件配置 eureka 地址
server:
port: 8081
spring:
application:
name: provider
eureka:
client:
# 配置eureka服务地址
service-url:
defaultZone: http://127.0.0.1:8099/eureka
服务发现:
- 引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 启动类添加 @EnableEurekaClient
- 配置文件配置 eureka 地址
server:
port: 8089
spring:
application:
name: user
eureka:
client:
# 配置eureka服务地址
service-url:
defaultZone: http://127.0.0.1:8099/eureka
Ribbo - 负载均衡
1. 负载均衡流程
Ribbon负载均衡流程图:
http://PROVIDER/pro/4 并非真实的地址,这个需要Ribbon负载均衡去拦截,然后选择具体的服务地址。而,Ribbon就是通过 LoadBalancerInterceptor 的 intercept 方法来实现拦截请求并解析选择地址。
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
负载均衡流程:
2. 负载均衡策略
Ribbon的负载均衡策略是由 IRule 接口来定义的。
自定义负载均衡策略:
- 方式一: 在user中,@Bean 注入自定义 IRule
@Bean
public IRule randomRule(){
return new RandomRule();
}
- 方式二: 在user,配置文件修改 IRule
provider: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意:
方式一的作用范围是:在访问任何微服务的时候,都是使用 RandomRule 负载均衡策略;
方式二的作用范围是:在访问 provider 微服务的时候才是采用 RandomRule 策略,其他的还是使用默认策略。
加载策略:
Ribbon默认采用懒加载,即会在第一次访问时才会去创建 LoadBalanceClient ,所以会在第一次请求的时候花上较长的等待时间。
可以通过配置文件更改加载策略为饿加载策略,即初始化时就创建 LoadBalanceClient ,降低第一次访问的耗时。
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: provider # 指定饥饿加载的服务名称
nacos
概念:
服务注册中心
作用:
心跳检查: 不同于 eureka 只能 Service实例 主动发起心跳,nacos 对于非临时实例可以主动发起心跳检查
临时心跳检查异常的会被剔除出 服务地址列表
- 非临时心跳检查异常的不会被剔除
- 定时推送变更: nacos 支持服务列表变更的消息推送模式,服务列表更新更及时
注册中心
引入依赖、修改配置
父工程添加 SpringCloudAlibaba 的管理依赖。
<!--spring-cloud-alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
子工程中注释掉 eureka 依赖,引入 nacos 依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置文件配置nacos地址:
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
启动 user 和 provider :
注意: eureka 注册名会变成大写,nacos 不会,所以改成 nacos 之后,需要把访问地址改成小写。
请求测试:
服务分级存储模型
一个服务可以拥有多个实例,如:provider 的 8081 和 8082,如果这些实例分布于全国不同的机房,如:provider:8081 在成都机房、provider:8082 在重庆机房,Nacos就将同一机房内的实例 划分为一个集群。
即,一个服务可以拥有多个集群,一个集群中可以拥有多个实例。
微服务相互之间访问时,访问本地的速度更快,所以应该尽可能访问相同集群的实例,只有当本集群内存不够时,才去访问其他集群。
1. 配置集群
配置服务集群:
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: CD
VM Options:
-Dserver.port=8082 -Dspring.cloud.nacos.discovery.cluster-name=CQ
2. 同集群优先的负载均衡
默认的 ZoneAvoidanceRule 负载均衡策略并不能实现据同集群优先来实现负载均衡,因此Nacos中提供了一个NacosRule
的实现,可以优先从同集群中挑选实例。
修改负载均衡策略:
- 方式一:
@Bean
public IRule nacosRule(){
return new NacosRule();
}
- 方式二:
provider: # 访问的服务名
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
3. 权重配置
服务器的性能之间存在差异,为了让性能能好的服务器承担更多的用户请求,Nacos提供了权重配置来控制访问率:权重越大、访问率越高。
注意: 权重为0的服务永远不会被访问。
4. 环境隔离
Nacos提供了namespace来实现环境隔离功能。
- nacos中可以有多个namespace
- namespace下可以有group、service等
- 不同namespace之间相互隔离(不同namespace的服务互相不可见)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/f2ab112e87e676f0c8ec940083c32215.png)
1. 创建 namespace
Nacos默认的namespace是:public
命名空间
Nacos创建命名空间流程:
2. 配置命名空间
spring:
cloud:
nacos:
discovery:
namespace: e11eb3bc-8eed-4cdd-93f0-7a6c01b85eb4 # 命名空间,填ID
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为永久实例
3. 配置管理
Nacos不仅可以担任微服务的注册中心,还可以担任配置管理。
统一配置管理
可以使用统一配置管理来处理因为部署的微服务数量过多,配置繁杂等问题。
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
注意: 只把那些需要热更新的配置文件交给Nacos
1. nacos添加配置文件
Data ID: 配置文件id:服务名称-profile.后缀名 。
2. 从nacos拉取配置
spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:
引入nacos-config依赖:
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
添加bootstrap.yml
spring:
application:
name: user # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
s
p
r
i
n
g
.
a
p
p
l
i
c
a
t
i
o
n
.
n
a
m
e
−
{spring.application.name}-
spring.application.name−{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。
读取nacos配置
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
启动报错: org.springframework.beans.factory.BeanCreationException: Error creating bean with name xxx
解决方案:
检查 namespace,配置文件与服务要在同一个 namespace 中
名称是否拼错
升级 nacos 版本
@Value 切换为 @NacosValue
配置热更新
配置热更新: 修改nacos中的配置后,微服务中无需重启即可让配置生效。
实现方式:
方式一: 在@Value注入的变量所在类上添加注解 @RefreshScope
方式二:
使用 @ConfigurationProperties 注解代替@Value注解
在 user 服务中,添加一个类,读取patterrn.dateformat属性:
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
在UserController中使用这个类代替@Value:
@Autowired
private PatternProperties patternProperties;
@GetMapping("now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat));
}
配置共享
其实微服务启动时,会去nacos读取多个配置文件,因为nacos管理的配置文件不包含环境信息,可以被多个环境共享。
只需要取名的时候,不加上 profile 就能被共享,如下:
配置共享时的优先级:
配置中心
Nacos
微服务-配置中心
微服务架构中存在很多服务,每个服务都有自己的配置文件,这些配置文件如果集成在服务中,也是难以维护的,于是便可以使用配置中心来管理这些配置文件。
nacos既可以作为 注册中心 ,也可以作为 配置中心 ,使用nacos配置中心,你可以将服务里的配置文件写在nacos上通过nacos面板来修改管理服务的配置而无需停止服务。
如果使用nacos作为注册中心而不作为配置中心,添加配置:spring.cloud.nacos.config.enabled =false 即可。
可以以请求方式发布、获取配置:
发布配置:post,http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=HelloWorld
获取配置:get,http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test
1.搭建服务端
nacos服务端搭建是十分容易的,只需下载+配置+启动就行,参考注册中心这篇:点击前往
建议:使用mysql作为持久化。
2.搭建客户端
客户端即每个配置要被管理的服务,服务的搭建还如往常一样搭建,只不过配置文件发生一些变化,以下就认为你已经创建了一个服务模块/项目,并以此起步。
1.客户端引入nacos config依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2.创建一个bootstrap.yml(或properties),配置nacos config的配置 :
spring:
cloud:
nacos:
config: #nacos的信息,添加后可以省略注册中心的nacos此类信息
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
contextPath: /nacos
#--------------------------
file-extension: yaml #显示的声明dataId文件扩展名
3.application.yml(或properties),除特殊配置外,只留下下面2个配置:
server:
port: 41150
spring:
application:
name: server-producer
到这里你会发现你有2个配置文件: bootstrap.yml、application.yml 。这时你可能就有疑问了,不是说好的将配置交给配置中心管理了吗,怎么服务里还变多了呢。
那么我来给你解释一下吧:
bootsteap.yml:这里只配置配置中心的信息,服务通过它去获取服务所需配置,而且这里的内容基本不变。
application.yml:这里只配置端口+服务名,因为首次启动服务时,需要通过它注册达到nacos,然后才能获取配置。
可以看出,上面的配置基本上就是固定的,而且基本上不会被修改,实际项目中配置文件远远不止这些,其他配置都将被放在配置中心统一管理。
4.在nacos配置中心添加配置文件 :
点击添加后页面如下:配置名要和服务名一致,后缀带不带都行。
第一次发布配置后,重启项目即可。之后修改配置都不需要启动服务。
服务与配置文件的匹配:
文件名=服务名,建议显示指定配置文件后缀。
多同名配置会互相覆盖,比如server、server.yml在指定yml格式后,两个文件会覆盖。
更多特性自己摸索,使用:服务名.yml 并 在bootstrap.yml指定格式为yaml 一定能用。
测试一下效果:按照阿里的示例进行举例。
在客户端创建一个测试类,并在nacos上配置中文件添加属性,然后注入到属性中,启动后控制台就会打印。
@Configuration
public class TestConfig {
@Value("${user.names}")
public String test1;
@Value("${user.pwds}")
public String test2;
@PostConstruct
public void getUser(){
System.out.println("name=" + test1 + "|pwd=" + test2);
}
}
启动后,会发现打印的数据就是配置中心上的数据了。
3.动态刷新
此节代码来自于:阿里云-知行动手实验室
配置发生变化后,服务中绑定的属性值将刷新,会刷新的情况有3种:
- 类上加注解:@RefreshScope、类种非静态属性加@Value注解
@RestController
@RefreshScope
public class NacosConfigDemo {
@Value("${user.name}")
private String userName;
@Value("${user.age}")
private int userAge;
@PostConstruct
public void init() {
System.out.printf("[init] user name : %s , age : %d%n", userName, userAge);
}
@RequestMapping("/user")
public String user() {
return String.format("[HTTP] user name : %s , age : %d", userName, userAge);
}
}
- 类上加注解:@RefreshScope、@ConfigurationProperties
@RefreshScope
@ConfigurationProperties(prefix = "user")
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 使用Nacos Config监听来对Bean属性实施监听。
@RestController
@RefreshScope
@EnableConfigurationProperties(User.class)
public class NacosConfigDemo {
@Value("${user.name}")
private String userName;
@Value("${user.age}")
private int userAge;
@PostConstruct
public void init() {
System.out.printf("[init] user name : %s , age : %d%n", userName, userAge);
}
@PreDestroy
public void destroy() {
System.out.printf("[destroy] user name : %s , age : %d%n", userName, userAge);
}
@RequestMapping("/user")
public String user() {
return String.format("[HTTP] user name : %s , age : %d", userName, userAge);
}
}
4.nacos config高可用
nacos是配置中心也是注册中心,实现nacos的高可用和 注册中心 高可用一样。
5.nacos config高级配置
namespace命名空间
nacos可以实现配置文件之间隔离,不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置,可以实现不同开发环境(开发、测试、生产等)配置文件的隔离。
创建命名空间也很容易
切换命名空间
创建后,在客户端bootstrap.yml添加如下配置进行绑定:
spring:
cloud:
nacos:
config:
namespace: c70a9f91-a687-427f-97bb-5a6e54289a6e
Group分组
在创建配置时可以指定分组,自定义分组在bootstrap.yml添加如下:
spring:
cloud:
nacos:
config:
group: DEVELOP_GROUP
自定义配置名
三种情况完整配置案例:
spring:
application:
name: myserver
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
extension-configs[0]: # 1.在默认组,不支持动态刷新
data-id: myconfig-1.yaml
extension-configs[1]: # 2.不在默认组,不支持动态刷新
data-id: myconfig-2.yaml
group: MY_GROUP
extension-configs[2]: # 3.不在默认的组,支持动态刷新
data-id: myconfig-3.yaml
group: MY_GROUP
refresh: true
总结:默认配置名+默认组时才会自动刷新,否则需指定refresh: true才自动刷新。
注意:
- 多个相同配置文件存在优先级extension-configs[a]里a越大,优先级越高。
- data-id结尾必须有properties/yaml/yml等扩展名。
Config
Apollo
Spring Cloud是目前微服务架构领域的翘楚,无数的书籍博客都在讲解这个技术。不过大多数讲解还停留在对Spring Cloud功能使用的层面,其底层的很多原理,很多人可能并不知晓。因此本文将通过大量的手绘图,给大家谈谈Spring Cloud微服务架构的底层原理。
实际上,Spring Cloud 是一个全家桶式的技术栈,包含了很多组件。本文先从其最核心的几个组件入手,来剖析一下其底层的工作原理。也就是 Eureka、Feign、Ribbon、Hystrix、Zuul 这几个组件。 EFRHZ
一、业务场景介绍
先来给大家说一个业务场景,假设咱们现在开发一个电商网站,要实现支付订单的功能,流程如下:
- 创建一个订单后,如果用户立刻支付了这个订单,我们需要将订单状态更新为“已支付”
- 扣减相应的商品库存
- 通知仓储中心,进行发货
- 给用户的这次购物增加相应的积分
针对上述流程,我们需要有: 订单服务、库存服务、仓储服务、积分服务。整个流程的大体思路如下:
- 用户针对一个订单完成支付之后,就会去找订单服务,更新订单状态
- 订单服务调用库存服务,完成相应功能
- 订单服务调用仓储服务,完成相应功能
- 订单服务调用积分服务,完成相应功能
至此,整个支付订单的业务流程结束
下图这张图,清晰表明了各服务间的调用过程:
好!有了业务场景之后,咱们就一起来看看Spring Cloud微服务架构中,这几个组件如何相互协作,各自发挥的作用以及其背后的原理。
二、Spring Cloud核心组件:Eureka
咱们来考虑第一个问题:订单服务想要调用库存服务、仓储服务,或者积分服务,怎么调用?
- 订单服务压根儿就不知道人家库存服务在哪台机器上啊!他就算想要发起一个请求,都不知道发送给谁,有心无力!
- 这时候,就轮到Spring Cloud Eureka出场了。Eureka是微服务架构中的注册中心,专门负责服务的注册与发现。
咱们来看看下面的这张图,结合图来仔细剖析一下整个流程:
如上图所示,库存服务、仓储服务、积分服务中都有一个Eureka Client组件,这个组件专门负责将这个服务的信息注册到Eureka Server中。说白了,就是告诉Eureka Server,自己在哪台机器上,监听着哪个端口。而Eureka Server是一个注册中心,里面有一个注册表,保存了各服务所在的机器和端口号
订单服务里也有一个Eureka Client组件,这个Eureka Client组件会找Eureka Server问一下:库存服务在哪台机器啊?监听着哪个端口啊?仓储服务呢?积分服务呢?然后就可以把这些相关信息从Eureka Server的注册表中拉取到自己本地缓存起来。
这时如果订单服务想要调用库存服务,不就可以找自己本地的Eureka Client问一下库存服务在哪台机器?监听哪个端口吗?收到响应后,紧接着就可以发送一个请求过去,调用库存服务扣减库存的那个接口!同理,如果订单服务要调用仓储服务、积分服务,也是如法炮制。
总结一下:
- Eureka Client:负责将这个服务的信息注册到Eureka Server中
- Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号
三、Spring Cloud核心组件:Feign
现在订单服务确实知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,跟其他服务建立网络连接,然后构造一个复杂的请求,接着发送请求过去,最后对返回的响应结果再写一大堆代码来处理吗?
这是上述流程翻译的代码片段,咱们一起来看看,体会一下这种绝望而无助的感受!!!
友情提示,前方高能:
看完上面那一大段代码,有没有感到后背发凉、一身冷汗?实际上你进行服务间调用时,如果每次都手写代码,代码量比上面那段要多至少几倍,所以这个事压根儿就不是地球人能干的。
既然如此,那怎么办呢?别急,Feign早已为我们提供好了优雅的解决方案。来看看如果用Feign的话,你的订单服务调用库存服务的代码会变成啥样?
看完上面的代码什么感觉?是不是感觉整个世界都干净了,又找到了活下去的勇气!没有底层的建立连接、构造请求、解析响应的代码,直接就是用注解定义一个 FeignClient 接口,然后调用那个接口就可以了。人家Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign全给你干了。
那么问题来了,Feign是如何做到这么神奇的呢?很简单,Feign的一个关键机制就是使用了动态代理。咱们一起来看看下面的图,结合图来分析:
- 首先,如果你对某个接口定义了**@FeignClient**注解,Feign就会针对这个接口创建一个动态代理
- 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
- Feign的动态代理会根据你在接口上的**@RequestMapping**等注解,来动态构造出你要请求的服务的地址
- 最后针对这个地址,发起请求、解析响应
四、Spring Cloud核心组件:Ribbon
说完了Feign,还没完。现在新的问题又来了,如果人家库存服务部署在了5台机器上,如下所示:
- 192.168.169:9000
- 192.168.170:9000
- 192.168.171:9000
- 192.168.172:9000
- 192.168.173:9000
这下麻烦了!人家Feign怎么知道该请求哪台机器呢?
- 这时Spring Cloud Ribbon就派上用场了。Ribbon就是专门解决这个问题的。它的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上
- Ribbon 的负载均衡默认使用的最经典的Round Robin轮询算法。这是啥?简单来说,就是如果订单服务对库存服务发起10次请求,那就先让你请求第1台机器、然后是第2台机器、第3台机器、第4台机器、第5台机器,接着再来—个循环,第1台机器、第2台机器。。。以此类推。
此外,Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:
- 首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。
- 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器
- Feign就会针对这台机器,构造并发起请求。
对上述整个过程,再来一张图,帮助大家更深刻的理解:
五、Spring Cloud核心组件:Hystrix
在微服务架构里,一个系统会有很多的服务。以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有100个线程可以处理请求,然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。
咱们一起来分析一下,这样会导致什么问题?
- 如果系统处于高并发的场景下,大量请求涌过来的时候,订单服务的100个线程都会卡在请求积分服务这块。导致订单服务没有一个线程可以处理请求
- 然后就会导致别人请求订单服务的时候,发现订单服务也挂了,不响应任何请求了
上面这个,就是微服务架构中恐怖的服务雪崩问题,如下图所示:
如上图,这么多服务互相调用,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。比如积分服务挂了,会导致订单服务的线程全部卡在请求积分服务这里,没有一个线程可以工作,瞬间导致订单服务也挂了,别人请求订单服务全部会卡住,无法响应。
但是我们思考一下,就算积分服务挂了,订单服务也可以不用挂啊!为什么?
- 我们结合业务来看:支付订单的时候,只要把库存扣减了,然后通知仓库发货就OK了
- 如果积分服务挂了,大不了等他恢复之后,慢慢人肉手工恢复数据!为啥一定要因为一个积分服务挂了,就直接导致订单服务也挂了呢?不可以接受!
现在问题分析完了,如何解决?
这时就轮到Hystrix闪亮登场了。Hystrix是隔离、熔断以及降级的一个框架。啥意思呢?说白了,Hystrix会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。
打个比方:现在很不幸,积分服务挂了,会咋样?
当然会导致订单服务里那个用来调用积分服务的线程都卡死不能工作了啊!但由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。
这个时候如果别人请求订单服务,订单服务还是可以正常调用库存服务扣减库存,调用仓储服务通知发货。只不过调用积分服务的时候,每次都会报错。**但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!**所以我们直接对积分服务熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!
**那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?**没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。
为帮助大家更直观的理解,接下来用一张图,梳理一下Hystrix隔离、熔断和降级的全流程:
六、Spring Cloud核心组件:Zuul
说完了Hystrix,接着给大家说说最后一个组件:Zuul,也就是微服务网关。**这个组件是负责网络路由的。**不懂网络路由?行,那我给你说说,如果没有Zuul的日常工作会怎样?
假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做inventory-service?部署在5台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那真是友谊的小船,说翻就翻!
上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面,像android、ios、pc前端、微信小程序、H5等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。
而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。
七、总结:使用及配置
最后再来总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:
-
Eureka:各个服务启动时,Eureka Client 都会将服务注册到 Eureka Server,并且 Eureka Client 还可以反过来从 Eureka Server 拉取注册表,从而知道其他服务在哪里
-
Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
-
Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
-
Hystrix:发起请求是通过 Hystrix 的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
-
Zuul:如果前端、移动端要调用后端系统,统一从 Zuul 网关进入,由Zuul网关转发请求给对应的服务
以上就是我们通过一个电商业务场景,阐述了 Spring Cloud 微服务架构几个核心组件的底层原理。
文字总结还不够直观?没问题!我们将Spring Cloud的5个核心组件通过一张图串联起来,再来直观的感受一下其底层的架构原理:
Eureka
一、erueka 客户端配置
1、Eureka 启禁用
eureka.client.enabled=true
2、Eureka 连接超时时间
eureka.client.eureka-server-connect-timeout-seconds=5
eureka.client.eureka-connection-idle-timeout-seconds=30
3、Eureka Server等待超时时间
eureka.client.eureka-server-read-timeout-seconds=8
4、拉取Eureka注册信息
eureka.client.fetch-registry=true
5、注册自身到Eureka
eureka.client.register-with-eureka=true
6、过滤状态存活的实例
eureka.client.filter-only-up-instances=true
7、注册实例的名称
eureka.instance.appname=unknown
8、健康检查相对路径
eureka.instance.health-check-url-path=/health
9、HOME页面相对路径
eureka.instance.home-page-url-path=/
10、服务续约到期时间
eureka.instance.lease-expiration-duration-in-seconds=90
11、服务续约间隔时间
eureka.instance.lease-renewal-interval-in-seconds=30
12、注册元数据
eureka.instance.metadata-map=
13、通过配置文件找到namespace,忽略springcloud的配置
eureka.instance.namespace=
14、注册是否显示IP地址(第一个非回环地址)
eureka.instance.prefer-ip-address=false
15、注册时使用的IP地址
eureka.instance.ip-address=
16、压缩Eureka Server的数据
eureka.client.g-zip-content=true
17、心跳执行器的线程池初始值
eureka.client.heartbeat-executor-thread-pool-size=2
18、最初复制实例信息到Eureka服务器所需的时间
eureka.client.initial-instance-info-replication-interval-seconds=40
19、同步实例变更信息到Eureka服务到周期
eureka.client.instance-info-replication-interval-seconds=30
20、从Eureka服务器拉取服务信息周期
eureka.client.registry-fetch-interval-seconds=30
21、Eureka Server地址
eureka.client.serviceUrl.defaultZone=xxx,xxx,xxx
二、eureka 服务端配置
1、注册实例
eureka.instance.registry.default-open-for-traffic-count=1
eureka.instance.registry.expected-number-of-renews-per-min=1
2、服务面板
eureka.dashborad.enabled=true
eureka.dashboard.path=/
3、自我保护
eureka.server.enable-self-preservation: true
eureka.server.eviction-interval-timer-in-ms: 60000
eureka.server.renewal-percent-threshold: 0.85
4、限流
eureka.server.rate-limiter-enabled: false
eureka.server.rate-limiter-throttle-standard-clients: false
eureka.server.rate-limiter-burst-size: 10
eureka.server.rate-limiter-full-fetch-average-rate: 100
eureka.server.rate-limiter-registry-fetch-average-rate: 500
5、相应缓存
eureka.server.response-cache-auto-expiration-in-seconds: 180
eureka.server.response-cache-update-interval-ms: 30000
eureka.server.use-read-only-response-cache: true
6、安全校验
#启用安全校验
security.basic.enabled=true
#授权用户名
security.user.name=root
#授权密码
security.user.password=123456
三、高可用配置
1、节点数据同步
eureka.server.batch-replication: false
eureka.enable-replicated-request-compression: false
2、同步线程池
eureka.server.min-threads-for-status-replication: 1
eureka.server.max-threads-for-status-replication: 1
eureka.server.max-idle-thread-in-minutes-age-for-status-replication: 10
eureka.server.min-threads-for-peer-replication: 5
eureka.server.max-threads-for-peer-replication: 20
eureka.server.max-idle-thread-age-in-minutes-for-peer-replication: 15
eureka.server.number-of-replication-retries: 5
3、定时任务
eureka.server.peer-eureka-status-refresh-time-interval-ms: 3000
eureka.server.peer-eureka-nodes-update-interval-ms: 600000
eureka.server.min-available-instances-for-peer-replication: -1
4、连接
eureka.server.peer-node-total-connections-per-host: 500
eureka.server.peer-node-total-connections: 1000
eureka.server.peer-node-read-timeout-ms: 200
eureka.server.peer-node-connect-timeout-ms: 200
eureka.server.peer-node-connection-idle-timeout-seconds: 30
5、重置注册
eureka.server.registry-sync-retries: 0
eureka.server.registry-sync-retry-wait-ms: 30000
Feign
使用及配置url: SpringCloud:Feign的使用及配置_feignclient配置_Poetry-Distance的博客-CSDN博客
Ribbon
配置及使用的链接: Ribbon介绍 (配置文件配置篇)_配置文件中ribbon.listofservers的作用-CSDN博客
Hystrix
使用及配置的url: Hystrix使用及其配置详解_hystrix配置_李子捌的博客-CSDN博客
@RestController
@RequestMapping("/circuitBreaker")
public class CircuitBreakConfigController {
@GetMapping
@HystrixCommand(
fallbackMethod = "fallback",
commandProperties = {
@HystrixProperty(
name = "execution.isolation.thread.timeoutInMilliseconds",
value = "1000"
),
@HystrixProperty(
name = "circuitBreaker.requestVolumeThreshold",
value = "4"
),
@HystrixProperty(
name = "circuitBreaker.errorThresholdPercentage",
value = "50"
),
@HystrixProperty(
name = "metrics.rollingStats.timeInMilliseconds",
value = "60000"
),
@HystrixProperty(
name = "circuitBreaker.sleepWindowInMilliseconds",
value = "60000"
)
})
public ResponseEntity<String> demo() {
try {
// 模拟接口请求
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new ResponseEntity<>("Hello!", HttpStatus.OK);
}
private ResponseEntity<String> fallback() {
return new ResponseEntity<>("Timeout!", HttpStatus.OK);
}
}
八、REDIS
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/4a0f4c316afd118cef3aba81dc6ed935.png)
什么是Redis
Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。
Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
Redis有5中数据类型:
- String (字符串)
- Hash (散列)
- List (列表)
- Set (集合)
- Zset (有序集合)
redis 持久化 —— RDB(Redis DataBase)和 AOF(Append Only File)
关于Redis,目前都是使用Redis作为数据缓存,缓存的目标主要是那些需要经常访问的数据,或计算复杂而耗时的数据。缓存的效果就是减少了数据库读的次数,减少了复杂数据的计算次数,从而提高了服务器的性能。
一、redis持久化----两种方式
1、redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
2、RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上;
3、AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
4、其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式来进行数据恢复,这是因为AOF方式的数据恢复完整度更高。
5、如果你没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。
二、redis持久化----RDB
1、RDB方式,是将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
2、redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。
3、对于RDB方式,redis会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,这样就确保了redis 极高的性能。
4、如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
5、虽然RDB有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么RDB方式就不太适合你,因为即使你每5分钟都持久化一次,当redis故障时,仍然会有近5分钟的数据丢失。所以,redis还提供了另一种持久化方式,那就是AOF。
三、redis持久化----AOF
1、AOF,英文是Append Only File,即只允许追加不允许改写的文件。
2、如前面介绍的,AOF方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍,就这么简单。
3、我们通过配置redis.conf中的appendonly yes就可以打开AOF功能。如果有写操作(如SET等),redis就会被追加到AOF文件的末尾。
4、默认的AOF持久化策略是每秒钟fsync一次(fsync是指把缓存中的写指令记录到磁盘中),因为在这种情况下,redis仍然可以保持很好的处理性能,即使redis故障,也只会丢失最近1秒钟的数据。
5、如果在追加日志时,恰好遇到磁盘空间满、inode满或断电等情况导致日志写入不完整,也没有关系,redis提供了redis-check-aof工具,可以用来进行日志修复。
6、因为采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,为此,redis提供了AOF文件重写(rewrite)机制,即当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了100次INCR指令,在AOF文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。
7、在进行AOF重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响AOF文件的可用性,这点大家可以放心。
8、AOF方式的另一个好处,我们通过一个“场景再现”来说明。某同学在操作redis时,不小心执行了FLUSHALL,导致redis内存中的数据全部被清空了,这是很悲剧的事情。不过这也不是世界末日,只要redis配置了AOF持久化方式,且AOF文件还没有被重写(rewrite),我们就可以用最快的速度暂停redis并编辑AOF文件,将最后一行的FLUSHALL命令删除,然后重启redis,就可以恢复redis的所有数据到FLUSHALL之前的状态了。是不是很神奇,这就是AOF持久化方式的好处之一。但是如果AOF文件已经被重写了,那就无法通过这种方法来恢复数据了。
9、虽然优点多多,但AOF方式也同样存在缺陷,比如在同样数据规模的情况下,AOF文件要比RDB文件的体积大。而且,AOF方式的恢复速度也要慢于RDB方式。
如果你直接执行BGREWRITEAOF命令,那么redis会生成一个全新的AOF文件,其中便包括了可以恢复现有数据的最少的命令集。
10、如果运气比较差,AOF文件出现了被写坏的情况,也不必过分担忧,redis并不会贸然加载这个有问题的AOF文件,而是报错退出。这时可以通过以下步骤来修复出错的文件:
1.备份被写坏的AOF文件
2.运行redis-check-aof –fix进行修复
3.用diff -u来看下两个文件的差异,确认问题点
4.重启redis,加载修复后的AOF文件
四、redis持久化----AOF重写
1、AOF重写的内部运行原理,我们有必要了解一下。
2、在重写即将开始之际,redis会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
3、与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
4、当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中。
5、当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中了。
五、redis持久化----如何选择RDB和AOF
1、对于我们应该选择RDB还是AOF,官方的建议是两个同时使用。这样可以提供更可靠的持久化方案。
2、redis的备份和还原,可以借助第三方的工具redis-dump。
六、Redis的两种持久化方式也有明显的缺点
1、RDB需要定时持久化,风险是可能会丢两次持久之间的数据,量可能很大。
2、AOF每秒fsync一次指令硬盘,如果硬盘IO慢,会阻塞父进程;风险是会丢失1秒多的数据;在Rewrite过程中,主进程把指令存到mem-buffer中,最后写盘时会阻塞主进程。
redids事务 ACID
Redis 的事务需要先划分出三个阶段
事务开启:使用 MULTI 可以标志着执行该命令的客户端从非事务状态切换至事务状态;
命令入队:MULTI开启事务之后,非 WATCH、EXEC、DISCARD、MULTI
等特殊命令,客户端的命令不会被立即执行,而是放入一个事务队列;
如果收到 EXEC 命令,事务队列里的命令将会被执行;
如果收到 DISCARD 命令,则事务被丢弃。
命令入队过程如果出错(如使用了不存在的命令),则事务队列会被拒接执行;
执行事务:执行事务期间出现了异常(如命令和操作的数据类型不匹配),事务队列的里的命令还是继续执行下去,直到全部命令执行完,不会回滚。
WATCH 可用于监控 redis 变量值,在命令 EXEC 之前,redis 里的数据是有机会被其他客户端的命令修改的。使用 WATCH
监控的变量被修改后,执行 EXEC 时则会返回执行失败的 nil 回复
从严格意义上来说,Redis 是没有事务的。因为事务必须具备四个特点:
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
Redis 是做不到这四点,只是具备其中一些特征,redis的事务是个伪事务,而且不支持回滚。
原子性
EXEC命令执行前:
在命令入队时就报错,(如内存不足,命令名称错误),redis 就会报错并且记录下这个错误。此时,客户还能继续提交命令操作;等到执行EXEC时,redis 就会拒绝执行所有提交的命令操作,返回事务失败的结果 nil。
EXEC命令执行后:
命令和操作的数据类型不匹配,但 redis 实例没有检查出错误。在执行完 EXEC 命令以后,redis 实际执行这些指令,就会报错。此时事务是不会回滚的,但事务队列的命令还是继续被执行。事务的原子性无法保证。
EXEC执行时发生故障:
如果 redis 开启了 AOF 日志,那么,只会有部分的事务操作被记录到 AOF 日志中。需要使用 redis-check-aof 工具检查 AOF 日志文件,这个工具可以把未完成的事务操作从 AOF 文件中去除。事务的原子性得到保证。
一致性
EXEC命令执行前:
入队报错事务会被放弃执行,具有一致性。
EXEC命令执行后:
实际执行时报错,错误的指令不会执行,正确的指令可以正常执行,一致性可以保证。
EXEC执行时发生故障:
RDB 模式,RDB 快照不会在事务执行时执行,事务结果不会保存在RDB;
AOF 模式,可以使用 redis-check-aof 工具检查 AOF 日志文件,把未完成的事务操作从 AOF 文件中去除。可以保证一致性。
隔离性
EXEC 命令执行前:
隔离性需要通过 WATCH 机制保证。因为 EXEC 命令执行前,其他客户端命令可以被执行,相关变量会被修改;但可以使用 WATCH 机制监控相关变量。一旦相关变量被修改,则 EXEC 后则事务失败返回,具有隔离性。
EXEC 命令执行后:
Redis 是单线程执行,事务队列里的命令和其他客户端的命令只能二选一被顺序执行,因此具有隔离性
持久性
如果 redis 没有使用 RDB 或 AOF,事务的持久化是不存在的;
RDB 模式:那么在一个事务执行后,而下一次的 RDB快照还未执行前,如果发生了实例宕机,数据丢失,这种情况下,事务修改的数据也是不能保证持久化;
AOF 模式:因为 AOF 模式的三种配置选项 no、everysec 和 always
都会存在数据丢失的情况。所以,事务的持久性属性也还是得不到保证。
总结
- Redis 的事务机制可以保证一致性和隔离性;
- 但是无法保证持久性;
- 具备了一定的原子性,但不支持回滚。
redis部署方案
standaloan(单机模式)
standaloan 是redis单机模式,及所有服务连接一台redis服务,该模式不适用生产。如果发生宕机,内存爆炸,就可能导致所有连接改redis的服务发生缓存失效引起雪崩。
ssentinel(哨兵模式)
redis-Sentinel(哨兵模式)是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行切换
redis集群模式,同样可以实现redis高可用部署,Redis Sentinel集群模式中,随着业务量和数据量增,到性能达到redis单节点瓶颈,垂直扩容受机器限制,水平扩容涉及对应用的影响以及数据迁移中数据丢失风险。针对这些痛点
Redis3.0推出cluster分布式集群方案,当遇到单节点内存,并发,流量瓶颈是,采用cluster方案实现负载均衡,cluster方案主要解决分片问题,即把整个数据按照规则分成多个子集存储在多个不同几点上,每个节点负责自己整个数据的一部分。
Redis Cluster 集群模式
redis cluster 采用哈希分区规则中的虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。Redis Cluster槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0 ~ 16383,计算公式:slot = CRC16(key)&16383。每一个实节点负责维护一部分槽以及槽所映射的键值数据。下图展现一个五个节点构成的集群,每个节点平均大约负责3276个槽,以及通过计算公式映射到对应节点的对应槽的过程。1.单机模式
优点:
架构简单,部署方便;
高性价比:缓存使用时无需备用节点(单实例可用性可以用supervisor或crontab保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务;
高性能。
缺点:
不保证数据的可靠性;
在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务;
高性能受限于单核CPU的处理能力(Redis是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用Memcached替代。
2.主从模式
Redis多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略。
优点:
高可靠性:一方面,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行;另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题;
读写分离策略:从节点可以扩展主库节点的读能力,有效应对大并发量的读操作。
缺点:
故障恢复复杂,如果没有RedisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其它从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐;
主库的写能力受到单机的限制,可以考虑分片;
主库的存储能力受到单机的限制,可以考虑Pika;
原生复制的弊端在早期的版本中也会比较突出,如:Redis复制中断后,Slave会发起psync,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时可能会造成毫秒或秒级的卡顿;又由于COW机制,导致极端情况下的主库内存溢出,程序异常退出或宕机;主库节点生成备份文件导致服务器磁盘IO和CPU(压缩)资源消耗;发送数GB大小的备份文件导致服务器出口带宽暴增,阻塞请求,建议升级到最新版本。
3.哨兵模式
Redis Sentinel是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel集群和Redis数据集群。
其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个。
锁的
优点:
Redis Sentinel集群部署简单;
能够解决Redis主从模式下的高可用切换问题;
很方便实现Redis数据节点的线形扩展,轻松突破Redis自身单线程瓶颈,可极大满足Redis大容量或高性能的业务需求;
可以实现一套Sentinel监控一组Redis数据节点或多组数据节点。
缺点:
部署相对Redis主从模式要复杂一些,原理理解更繁琐;
资源浪费,Redis数据节点中slave节点作为备份节点不提供服务;
Redis Sentinel主要是针对Redis数据节点中的主节点的高可用切换,对Redis的数据节点做失败判定分为主观下线和客观下线两种,对于Redis的从节点有对节点做主观下线操作,并不执行故障转移。
不能解决读写分离问题,实现起来相对复杂。
建议:
如果监控同一业务,可以选择一套Sentinel集群监控多组Redis数据节点的方案,反之选择一套Sentinel监控一组Redis数据节点的方案。
sentinel monitor 配置中的建议设置成Sentinel节点的一半加1,当Sentinel部署在多个IDC的时候,单个IDC部署的Sentinel数量不建议超过(Sentinel数量 – quorum)。
合理设置参数,防止误切,控制切换灵敏度控制:
a. quorum
b. down-after-milliseconds 30000
c. failover-timeout 180000
d. maxclient
e. timeout
部署的各个节点服务器时间尽量要同步,否则日志的时序性会混乱。
Redis建议使用pipeline和multi-keys操作,减少RTT次数,提高请求效率。
自行搞定配置中心(zookeeper),方便客户端对实例的链接访问。
4.集群方式
Redis Cluster是社区版推出的Redis分布式集群解决方案,主要解决Redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster能起到很好的负载均衡的目的。
Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。
优点:
无中心架构;
数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布;
可扩展性:可线性扩展到1000多个节点,节点可动态添加或删除;
高可用性:部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升;
降低运维成本,提高系统的扩展性和可用性。
缺点:
Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。
节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。
数据通过异步复制,不保证数据的强一致性。
多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
Slave在集群中充当“冷备”,不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率。
Key批量操作限制,如使用mset、mget目前只支持具有相同slot值的Key执行批量操作。对于映射为不同slot值的Key由于Keys不支持跨slot查询,所以执行mset、mget、sunion等操作支持不友好。
Key事务操作支持有限,只支持多key在同一节点上的事务操作,当多个Key分布于不同的节点上时无法使用事务功能。
Key作为数据分区的最小粒度,不能将一个很大的键值对象如hash、list等映射到不同的节点。
不支持多数据库空间,单机下的redis可以支持到16个数据库,集群模式下只能使用1个数据库空间,即db 0。
复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
避免产生hot-key,导致主库节点成为系统的短板。
避免产生big-key,导致网卡撑爆、慢查询等。
重试时间应该大于cluster-node-time时间。
Redis Cluster不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。
Redis持久化
由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。那么这两种持久化方式有什么区别呢,改如何选择呢?网上看了大多数都是介绍这两种方式怎么配置,怎么使用,就是没有介绍二者的区别,在什么应用场景下使用。
2、二者的区别
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
3、二者优缺点
RDB存在哪些优势呢?
1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
RDB又存在哪些劣势呢?
1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF的优势有哪些呢?
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
AOF的劣势有哪些呢?
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。
4、常用配置
RDB持久化配置
Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
AOF持久化配置
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
5、Redis自研
Redis自研的高可用解决方案,主要体现在配置中心、故障探测和failover的处理机制上,通常需要根据企业业务的实际线上环境来定制化。
优点:
高可靠性、高可用性;
自主可控性高;
贴切业务实际需求,可缩性好,兼容性好。
缺点:
实现复杂,开发成本高;
需要建立配套的周边设施,如监控,域名服务,存储元数据信息的数据库等;
维护成本高。
Redis有哪些优缺点
优点
读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化,支持AOF和RDB两种持久化方式。
支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点
数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
为什么要用 Redis /为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/dbae9c33fed492871d8cd931b4c52421.png)
为什么要用 Redis 而不用 map/guava 做缓存?
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路 I/O 复用模型,非阻塞 IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis有哪些数据类型
Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求
Redis的应用场景
总结一
计数器
可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
会话缓存
可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
查找表
例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
消息队列(发布/订阅功能)
List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
其它
Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。
总结二
Redis相比其他缓存,有一个非常大的优势,就是支持多种数据类型。
数据类型说明string字符串,最简单的k-v存储hashhash格式,value为field和value,适合ID-Detail这样的场景。list简单的list,顺序列表,支持首位或者末尾插入数据set无序list,查找速度快,适合交集、并集、差集处理sorted set有序的set
其实,通过上面的数据类型的特性,基本就能想到合适的应用场景了。
string——适合最简单的k-v存储,类似于memcached的存储结构,短信验证码,配置信息等,就用这种类型来存储。
hash——一般key为ID或者唯一标示,value对应的就是详情了。如商品详情,个人信息详情,新闻详情等。
list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的***,消息队列等。
set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set最牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:查找两个人共同的好友等。
Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。
如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很多场景,比一般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅有利于提升开发效率,也能有效利用Redis的性能。
什么是Redis持久化?
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis 的持久化机制是什么?各自的优缺点?
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:
RDB:是 Redis DataBase 缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
1、只有一个文件 dump.rdb,方便持久化。
2、容灾性好,一个文件可以保存到安全的磁盘。
3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
4.相对于数据集大时,比 AOF 的启动效率更高。
缺点:
1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
2、AOF(Append-only file)持久化方式: 是指所有的命令行记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof 文件。
AOF:持久化
AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
优点:
1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))
缺点:
1、AOF 文件比 RDB 文件大,且恢复速度慢。
2、数据集大的时候,比 rdb 启动效率低。
优缺点是什么?
AOF文件比RDB更新频率高,优先使用AOF还原数据。
AOF比RDB更安全也更大
RDB性能比AOF好
如果两个都配了优先加载AOF
如何选择合适的持久化方式
一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
Redis持久化数据和缓存怎么做扩容?
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
过期键的删除策略
Redis的过期键的删除策略
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
Redis key的过期时间和永久有效分别怎么设置?
EXPIRE和PERSIST命令。
我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
定时去清理过期的缓存;
当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
内存相关
MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
Redis的内存淘汰策略有哪些
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
全局的键空间选择性移除
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
总结
Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。
Redis主要消耗什么物理资源?
内存。
Redis的内存用完了会发生什么?
如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。
Redis如何做内存优化?
可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面
线程模型
Redis线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
参考:https://www.cnblogs.com/barrywxx/p/8570821.html
事务
什么是事务?
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis事务的概念
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis事务的三个阶段
事务开始 MULTI
命令入队
事务执行 EXEC
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队
Redis事务相关命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
如果在一个事务中出现运行错误,那么正确的命令会被执行。
WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
UNWATCH命令可以取消watch对所有key的监控。
事务管理(ACID)概述
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
多个事务并发执行时,一个事务的执行不应影响其他事务的执行
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。
Redis事务支持隔离性吗
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
Redis事务保证原子性吗,支持回滚吗
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis事务其他实现
基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,
其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐
Redis 主从架构
单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。
redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发
redis replication 的核心机制
redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
一个 master node 是可以配置多个 slave node 的;
slave node 也可以连接其他的 slave node;
slave node 做复制的时候,不会 block master node 的正常工作;
slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。
注意,如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。
另外,master 的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能确保启动的时候,是有数据的,即使采用了后续讲解的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。
redis 主从复制的核心原理
当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。
如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,
同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,
接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。
slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
过程原理
当从库和主库建立MS关系后,会向主数据库发送SYNC命令
主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致
缺点
所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决
Redis集群的主从复制模型是怎样的?
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品
生产环境中的 redis 是怎么部署的?
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
说说Redis哈希槽的概念?
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
Redis集群会有写操作丢失吗?为什么?
Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。
Redis集群之间是如何复制的?
异步复制
Redis集群最大节点个数是多少?
16384个
Redis集群如何选择数据库?
Redis集群目前无法做数据库选择,默认在0数据库。
分区
Redis是单线程的,如何提高多核CPU的利用率?
可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。
为什么要做Redis分区?
分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
你知道有哪些Redis分区实现方案?
客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。
Redis分区有什么缺点?
涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
同时操作多个key,则不能使用Redis事务.
分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set)
当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。
分布式问题
Redis实现分布式锁
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:设置成功,返回 1 。设置失败,返回 0 。
九、mybatis
简介结构图
Mybatis缓存(一级缓存、二级缓存)
Mybatis的一级、二级缓存
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同 , 二级缓存 默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
MyBatis是什么?
MyBatis 是一款优秀的持久层框架,一个半 ORM(对象关系映射)框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
ORM是什么
ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。
为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?
Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。
传统JDBC开发存在的问题
频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。可以使用连接池解决这个问题。但是使用jdbc需要自己实现连接池。
sql语句定义、参数设置、结果集处理存在硬编码。实际项目中sql语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译,重新发布。不好维护。
使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。
结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便。
mybatis的实现原理
参考文章:mybatis实现原理
JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的?
1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。
2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决: Mybatis自动将java对象映射至sql语句。
4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象。
Mybatis优缺点
优点
与传统的数据库访问技术相比,ORM有以下优点:
- 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
- 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)
- 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护
- 能够与Spring很好的集成
缺点
- SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求
- SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库
- MyBatis框架适用场景
MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。
对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。
Hibernate 和 MyBatis 的区别
相同点
都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。
不同点
映射关系
MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单
Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂
SQL优化和移植性
Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。
MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。
开发难易程度和学习成本
Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统
MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统
总结
MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架,
Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。
MyBatis的解析和运行原理
MyBatis编程步骤是什么样的?
1、 创建SqlSessionFactory
2、 通过SqlSessionFactory创建SqlSession
3、 通过sqlsession执行数据库操作
4、 调用session.commit()提交事务
5、 调用session.close()关闭会话
MyBatis的工作原理
在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图
1)读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
2)加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
3)构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
4)创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
5)Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
6)MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
7)输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
8)输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。
MyBatis的功能架构是怎样的
把Mybatis的功能架构分为三层:
API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
MyBatis的框架架构设计是怎么样的
架构图如下
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/140549b1d254d4c5d987ae78a96e61b6.png)
这张图从上往下看。MyBatis的初始化,会从mybatis-config.xml配置文件,解析构造成Configuration这个类,就是图中的红框。
(1)加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
(2)SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。
(3)SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。
(4)结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。
为什么需要预编译
定义:
SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译,这样 DBMS 执行 SQL 时,就不需要重新编译。
为什么需要预编译
JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下,将对所有的 SQL 进行预编译。
Mybatis都有哪些Executor执行器?它们之间的区别是什么?
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。
BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
Mybatis中如何指定使用哪一种Executor执行器?
在Mybatis配置文件中,在设置(settings)可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数,如SqlSession openSession(ExecutorType execType)。
配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。
Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
映射器
#{}和${}的区别
#{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。
Mybatis在处理**#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement**的set方法来赋值。
Mybatis在处理时 , 是 原 值 传 入 , 就 是 把 {}时,是原值传入,就是把时,是原值传入,就是把{}替换成变量的值,相当于JDBC中的Statement编译
变量替换后,#{} 对应的变量自动加上单引号 ‘’;变量替换后,${} 对应的变量不会加上单引号 ‘’
#{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入
#{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外
模糊查询like语句该怎么写
(1)’%${question}%’ 可能引起SQL注入,不推荐
(2)“%”#{question}“%” 注意:因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ',所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果。
(3)CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,推荐
(4)使用bind标签
<select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
<bind name="pattern" value="'%' + username + '%'" />
select id,sex,age,username,password from person where username LIKE #{pattern}
</select>
在mapper中如何传递多个参数
方法1:顺序传参法
public User selectUser(String name, int deptId);
<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{0} and dept_id = #{1}
</select>
#{}里面的数字代表传入参数的顺序。
这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错。
方法2:@Param注解传参法
public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);
<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
#{}里面的名称对应的是注解@Param括号里面修饰的名称。
这种方法在参数不多的情况还是比较直观的,推荐使用。
方法3:Map传参法
public User selectUser(Map<String, Object> params);
<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
#{}里面的名称对应的是Map里面的key名称。
这种方法适合传递多个参数,且参数易变能灵活传递的情况。
方法4:Java Bean传参法
public User selectUser(User user);
<select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
#{}里面的名称对应的是User类里面的成员属性。
这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。
Mybatis如何执行批量操作
使用foreach标签
foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。
item 表示集合中每一个元素进行迭代时的别名,随便起的变量名;
index 指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用;
open 表示该语句以什么开始,常用“(”;
separator表示在每次进行迭代之间以什么符号作为分隔符,常用“,”;
close 表示以什么结束,常用“)”。
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况:
- 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list
- 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array
- 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key
具体用法如下:
<!-- 批量保存(foreach插入多条数据两种方法)
int addEmpsBatch(@Param("emps") List<Employee> emps); -->
<!-- MySQL下批量保存,可以foreach遍历 mysql支持values(),(),()语法 --> //推荐使用
<insert id="addEmpsBatch">
INSERT INTO emp(ename,gender,email,did)
VALUES
<foreach collection="emps" item="emp" separator=",">
(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
</foreach>
</insert>
<!-- 这种方式需要数据库连接属性allowMutiQueries=true的支持
如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true -->
<insert id="addEmpsBatch">
<foreach collection="emps" item="emp" separator=";">
INSERT INTO emp(ename,gender,email,did)
VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})
</foreach>
</insert>
使用ExecutorType.BATCH
Mybatis内置的ExecutorType有3种,默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优; 但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的
具体用法如下:
//批量保存方法测试
@Test
public void testBatch() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
//可以执行批量操作的sqlSession
SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
//批量保存执行前时间
long start = System.currentTimeMillis();
try {
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
for (int i = 0; i < 1000; i++) {
mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
}
openSession.commit();
long end = System.currentTimeMillis();
//批量保存执行后的时间
System.out.println("执行时长" + (end - start));
//批量 预编译sql一次==》设置参数==》10000次==》执行1次 677
//非批量 (预编译=设置参数=执行 )==》10000次 1121
} finally {
openSession.close();
}
}
mapper和mapper.xml如下
public interface EmployeeMapper {
//批量保存员工
Long addEmp(Employee employee);
}
<mapper namespace="com.jourwon.mapper.EmployeeMapper"
<!--批量保存员工 -->
<insert id="addEmp">
insert into employee(lastName,email,gender)
values(#{lastName},#{email},#{gender})
</insert>
</mapper>
如何获取生成的主键
对于支持主键自增的数据库(MySQL)
<insert id="insertUser" useGeneratedKeys="true" keyProperty="userId" >
insert into user(
user_name, user_password, create_time)
values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP})
</insert>
parameterType 可以不写,Mybatis可以推断出传入的数据类型。如果想要访问主键,那么应当parameterType 应当是java实体或者Map。这样数据在插入之后 可以通过ava实体或者Map 来获取主键值。通过 getUserId获取主键
不支持主键自增的数据库(Oracle)
对于像Oracle这样的数据,没有提供主键自增的功能,而是使用序列的方式获取自增主键。
可以使用<selectKey>标签来获取主键的值,这种方式不仅适用于不提供主键自增功能的数据库,也适用于提供主键自增功能的数据库
<selectKey>一般的用法
<selectKey keyColumn="id" resultType="long" keyProperty="id" order="BEFORE">
</selectKey>
<insert id="insertUser" >
<selectKey keyColumn="id" resultType="long" keyProperty="userId" order="BEFORE">
SELECT USER_ID.nextval as id from dual
</selectKey>
insert into user(
user_id,user_name, user_password, create_time)
values(#{userId},#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP})
</insert>
此时会将Oracle生成的主键值赋予userId变量。这个userId 就是USER对象的属性,这样就可以将生成的主键值返回了。如果仅仅是在insert语句中使用但是不返回,此时keyProperty=“任意自定义变量名”,resultType 可以不写。
Oracle 数据库中的值要设置为 BEFORE ,这是因为 Oracle中需要先从序列获取值,然后将值作为主键插入到数据库中。
扩展
如果Mysql 使用selectKey的方式获取主键,需要注意下面两点:
order : AFTER
获取递增主键值 :SELECT LAST_INSERT_ID()
当实体类中的属性名和表中的字段名不一样 ,怎么办
第1种: 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
<select id="getOrder" parameterType="int" resultType="com.jourwon.pojo.Order">
select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select>
第2种: 通过<resultMap>
来映射字段名和实体类属性名的一一对应的关系。
<select id="getOrder" parameterType="int" resultMap="orderResultMap">
select * from orders where order_id=#{id}
</select>
<resultMap type="com.jourwon.pojo.Order" id="orderResultMap">
<!–用id属性来映射主键字段–>
<id property="id" column="order_id">
<!–用result属性来映射非主键字段,property为实体类属性名,column为数据库表中的属性–>
<result property ="orderno" column ="order_no"/>
<result property="price" column="order_price" />
</reslutMap>
Mapper 编写有哪几种方式?
第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。
(1)在 sqlMapConfig.xml 中配置 mapper.xml 的位置
<mappers>
<mapper resource="mapper.xml 文件的地址" />
<mapper resource="mapper.xml 文件的地址" />
</mappers>
(2)定义 mapper 接口
(3)实现类集成 SqlSessionDaoSupport
mapper 方法中可以 this.getSqlSession()进行数据增删改查。
(4)spring 配置
<bean id=" " class="mapper 接口的实现">
<property name="sqlSessionFactory"
ref="sqlSessionFactory"></property>
</bean>
第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean:
(1)在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和mappre 接口的名称相同且在同一个目录,这里可以不用配置
<mappers>
<mapper resource="mapper.xml 文件的地址" />
<mapper resource="mapper.xml 文件的地址" />
</mappers>
(2)定义 mapper 接口:
(3)mapper.xml 中的 namespace 为 mapper 接口的地址
(4)mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致
(5)Spring 中定义
<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="mapper 接口地址" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
第三种:使用 mapper 扫描器:
(1)mapper.xml 文件编写:
mapper.xml 中的 namespace 为 mapper 接口的地址;
mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致;
如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。
(2)定义 mapper 接口:
注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录
(3)配置 mapper 扫描器:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper 接口包地址
"></property>
<property name="sqlSessionFactoryBeanName"
value="sqlSessionFactory"/>
</bean>
(4)使用扫描器后从 spring 容器中获取 mapper 的实现对象。
什么是MyBatis的接口绑定?有哪些实现方式?
接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式
通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;
通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。
使用MyBatis的mapper接口调用时有哪些要求?
1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同。
2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。
3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。
4、Mapper.xml文件中的namespace即是mapper接口的类路径。
最佳实践中,通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗
Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MappedStatement对象。
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。
Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。
原因就是namespace+id是作为Map<String, MappedStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。
简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系?
答:Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。在Xml映射文件中,标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。每一个、、、标签均会被解析为MappedStatement对象,标签内的sql会被解析为BoundSql对象。
Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
第一种是使用标签,逐一定义列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列别名书写为对象属性名,比如T_NAME AS NAME,对象属性名一般是name,小写,但是列名不区分大小写,Mybatis会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成T_NAME AS NaMe,Mybatis一样可以正常工作。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?
还有很多其他的标签,、、、、,加上动态sql的9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind等,其中为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。
Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问,B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?
虽然Mybatis解析Xml映射文件是按照顺序解析的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。
原理是,Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时,Mybatis会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。
高级查询
MyBatis实现一对一,一对多有几种方式,怎么操作的?
有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的association,collection节点配置一对一,一对多的类就可以完成
嵌套查询是先查一个表,根据这个表里面的结果的外键id,去再另外一个表里面查询数据,也是通过配置association,collection,但另外一个表的查询通过select节点配置。
Mybatis是否可以映射Enum枚举类?
Mybatis可以映射枚举类,不单可以映射枚举类,Mybatis可以映射任何对象到表的一列上。映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter() 和 getResult() 接口方法。
TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter() 和 getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。
动态SQL
Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不?
Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。
插件模块
Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
举例:select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10
简述Mybatis的插件运行原理,以及如何编写一个插件。
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的**invoke()**方法,当然,只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写**intercept()**方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。