单点登录
sso:单点登录,字需要登录一次,就可以访问所有信任的应用系统。
单体架构下,单个tomcat服务session可以共享;微服务下多个tomcat服务session不共享。
常见解决方案:
JWT
Oauth2
CAS
JWT解决单点登录流程:
1.用户访问其他系统,会在网关判断token是否有效
2.如果token无效,前端跳转登录页面
3.用户发送登录请求,返回浏览器token,浏览器把token保存到cookie
4.再去访问其他服务的时候,都携带token,有网关统一验证后路由到目标服务
认证授权
认证:你是谁。
授权:你有权限干什么。
RBAC模型:基于角色的权限访问控制。这是一种通过角色关联权限,角色同时又关联用户的授权的方式。
在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。
常用框架:Apache shiro、Spring security(推荐)
数据安全
1.上传的数据安全性如何控制?
可以使用非对称加密(对称加密),给前端一个公钥让他把数据加密后传到后台,后台负责解密某对数据进行处理。
文件很大建议使用对称加密,不过不能保存敏感信息;
文件较小,要求安全性高,建议采用非对称加密;
2.数据脱敏
就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 * 来代替。
3.敏感词过滤
系统需要对用户输入的文本进行敏感词过滤如色情、政治、暴力相关的词汇。
常用方式:Trie 树算法 和 DFA 算法
Trie 树 也称为字典树、单词查找树,哈系树的一种变种,通常被用于字符串匹配,用来解决在一组字符串集合中快速查找某个字符串的问题。
假如我们的敏感词库中有以下敏感词:
- 高清有码
- 高清 AV
我们构造出来的敏感词 Trie 树就是下面这样的:
当我们要查找对应的字符串“东京热”的话,我们会把这个字符串切割成单个的符“东”、“京”、“热”,然后我们从 Trie 树的根节点开始匹配。
可以看出, Trie 树的核心原理其实很简单,就是通过公共前缀来提高字符串匹配效率。
用法:
Apache Commons Collecions 这个库中就有 Trie 树实现:
Trie<String, String> trie = new PatriciaTrie<>();
trie.put("Abigail", "student");
trie.put("Abi", "doctor");
trie.put("Annabel", "teacher");
trie.put("Christina", "student");
trie.put("Chris", "doctor");
Assertions.assertTrue(trie.containsKey("Abigail"));
assertEquals("{Abi=doctor, Abigail=student}", trie.prefixMap("Abi").toString());
assertEquals("{Chris=doctor, Christina=student}", trie.prefixMap("Chr").toString());
消息推送
消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。
消息推送一般又分为 Web 端消息推送和移动端消息推送。
常见方案:
短轮询
短轮询:在指定的时间间隔,由浏览器向服务器发出 HTTP 请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。
如:简单的 JS 定时器,每秒钟请求一次未读消息数接口,返回的数据展示即可。
setInterval(() => {
// 方法请求
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data
}
})
}, 1000);
但由于推送数据并不会频繁变更,无论后端此时是否有新的消息产生,客户端都会进行请求,势必会对服务端造成很大压力,浪费带宽和服务器资源。
长轮询
长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。
长轮询其实原理跟轮询差不多,都是采用轮询的方式。不过,如果服务端的数据没有发生变更,会 一直 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询。
缺点:长轮询相比于短轮询在性能上提升了很多,但依然会产生较多的请求
iframe
iframe 流就是在页面中插入一个隐藏的<iframe>
标签,通过在src
中请求消息数量 API 接口,由此在服务端和客户端之间创建一条长连接,服务端持续向iframe
传输数据。
iframe 流的服务器开销很大,而且IE、Chrome等浏览器一直会处于 loading 状态,图标会不停旋转,frame 流非常不友好,强烈不推荐。
SSE
SSE:服务器发送事件,服务器端到客户端(浏览器)的单向消息推送;
SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:
- SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。
- SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
- SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。
- SSE 默认支持断线重连;WebSocket 则需要自己实现。
- SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。
在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新等场景,SEE 不管是从实现的难易和成本上都更加有优势。
前端只需进行一次 HTTP 请求,带上唯一 ID,打开事件流,监听服务端推送的事件就可以了:
<script>
let source = null;
let userId = 7777
if (window.EventSource) {
// 建立连接
source = new EventSource('http://localhost:7777/sse/sub/'+userId);
setMessageInnerHTML("连接用户=" + userId);
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("建立连接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
</script>
服务端的实现更简单,创建一个SseEmitter
对象放入sseEmitterMap
进行管理:
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
/**
* 创建连接
*/
public static SseEmitter connect(String userId) {
try {
// 设置超时时间,0表示不过期。默认30秒
SseEmitter sseEmitter = new SseEmitter(0L);
// 注册回调
sseEmitter.onCompletion(completionCallBack(userId));
sseEmitter.onError(errorCallBack(userId));
sseEmitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId, sseEmitter);
count.getAndIncrement();
return sseEmitter;
} catch (Exception e) {
log.info("创建新的sse连接异常,当前用户:{}", userId);
}
return null;
}
/**
* 给指定用户发送消息
*/
public static void sendMessage(String userId, String message) {
if (sseEmitterMap.containsKey(userId)) {
try {
sseEmitterMap.get(userId).send(message);
} catch (IOException e) {
log.error("用户[{}]推送异常:{}", userId, e.getMessage());
removeUser(userId);
}
}
}
WebSocket
是一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
服务端使用@ServerEndpoint
注解标注当前类为一个 WebSocket 服务器,客户端可以通过ws://localhost:7777/webSocket/10086
来连接到 WebSocket 服务器端:
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
// 用来存在线连接数
private static final Map<String, Session> sessionPool = new HashMap<String, Session>();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
try {
this.session = session;
webSockets.add(this);
sessionPool.put(userId, session);
log.info("websocket消息: 有新的连接,总数为:" + webSockets.size());
} catch (Exception e) {
}
}
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message) {
log.info("websocket消息: 收到客户端消息:" + message);
}
/**
* 此为单点消息
*/
public void sendOneMessage(String userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("websocket消: 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
前端初始化打开 WebSocket 连接,并监听连接状态,接收服务端数据或向服务端发送数据:
<script>
var ws = new WebSocket('ws://localhost:7777/webSocket/10086');
// 获取连接状态
console.log('ws连接状态:' + ws.readyState);
//监听是否连接成功
ws.onopen = function () {
console.log('ws连接状态:' + ws.readyState);
//连接成功则发送一个数据
ws.send('test1');
}
// 接听服务器发回的信息并处理展示
ws.onmessage = function (data) {
console.log('接收到来自服务器的消息:');
console.log(data);
//完成通信后关闭WebSocket连接
ws.close();
}
// 监听连接关闭事件
ws.onclose = function () {
// 监听整个过程中websocket的状态
console.log('ws连接状态:' + ws.readyState);
}
// 监听并处理error事件
ws.onerror = function (error) {
console.log(error);
}
function sendMessage() {
var content = $("#message").val();
$.ajax({
url: '/socket/publish?userId=10086&message=' + content,
type: 'GET',
data: { "id": "7777", "content": content },
success: function (data) {
console.log(data)
}
})
}
</script>
MQTT
MQTT是一种基于发布/订阅模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网中的一个标准传输协议。
该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似。
总结:
定时任务
某些场景往往都要求我们在某个特定的时间去做某个事情,所以就有了定时任务。
单机:
我们直接通过 Spring 提供的 @Scheduled
注解即可定义定时任务,非常方便!
/**
* cron:使用Cron表达式。 每分钟的1,2秒运行
*/
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression() {
log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}
但Spring 自带的定时调度只支持单机,并且提供的功能比较单一。
并发:
如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。
XXL-JOB
于 2015 年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能
@XxlJob("myAnnotationJobHandler")
public ReturnT<String> myAnnotationJobHandler(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、内置了 UI 管理控制台。
项目中日志采集
我们搭建了ELK日志采集系统
介绍ELK的三个组件:
Elasticsearch是全文搜索分析引擎,可以对数据存储、搜索、分析;
Logstash是一个数据收集引擎,可以动态收集数据,可以对数据进行过滤、分析,将数据存 储到指定的位置;
Kibana是一个数据分析和可视化平台,配合Elasticsearch对数据进行搜索,分析,图表化展示;
常见日志命令
查看日志文件:tail -f xxx.log;
查看日志文件最后100行:tail -100f xxx.log;
文件中关键词日志:cat -n xxx.log | grep "error";
筛选过滤后,输出到一个文件:cat -n xxx.log | grep "error" > error.txt;
生产问题排查
1.先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题。
2.远程debug,查看错误信息。
如何快速定位系统瓶颈
压测(性能测试)
压测目的:给出系统当前的性能状况;定位系统性能瓶颈或潜在性能瓶颈
指标:响应时间、 QPS、并发数、吞吐量、CPU利用率、内存使用率、磁盘10、错误率
压测工具:Apache Jmeter ..
后端工程师:根据压测的结果进行解决或调优(接口、代码报错、并发达不到要求...)
java进程突然挂了如何排查
1.日志分析:查看应用程序日志,寻找报错、异常或警告信息
2.查看堆转储文件和线程转储文件:以查看是否有大量内存对象被使用导致溢出
3.系统资源检查:检查CPU、内存、磁盘的使用情况,以确定进程挂掉是否由资源不足引起
4.分析代码:检查是否有代码变更、新功能上线或第三方库引入导致的不兼容问题
5.检查jvm参数和环境:堆大小配置是否合适
6.监控系统:通过监控报警信息来定位问题
线上接口响应慢如何排查
1.查看日志,便宜分析;
2.定位范围:确认是所有接口响应慢还是特定接口慢。如果是所有接口都慢,可能是服务器资源、数据库或网络层的问题;如果是特定接口慢,则可能是业务逻辑或数据库查询的问题;分析问题是否在特定时间段出现,时段性问题可能与服务器负载、流量高峰、定时任务有关;检查问题是否与特定用户或请求相关,如果只发生在特定用户或请求中,可能与用户的数据量、请求参数相关
java死锁如何排查
1.使用jdk工具:通过jconsole可以连接到Java进程,并在“线程”标签页中查看死锁信息,包括死锁的线程和相应的堆栈跟踪;
2.代码级别检测:使用ThreadMXBean
API来检测死锁。ThreadMXBean
提供了findDeadlockedThreads
方法,该方法可以返回当前死锁的线程ID数组,从而可以进一步分析死锁情况
3.避免死锁:确保线程以一致的顺序获取锁,避免因不同的锁获取顺序导致的死锁;尽量减少锁的使用,或者使用其他并发控制机制