Nginx

简介

Nginx是一个高性能的HTTP和反向代理服务器,它以其高并发处理能力和低内存占用而著称,在静态内容处理方面占有优势,因此实际生产中,经常与tomcat结合使用(tomcat为重量型服务器,处理高并发能力较弱),Nginx可以作为Tomcat的前端服务器,提供静态文件服务(如HTML、CSS、JavaScript等),并作为反向代理服务器将请求转发给Tomcat。

优势

  • 提高访问速度

    在单次请求和高并发请求环境下,Nginx都会比其他web服务器相应的更快,Nginx之所以有这么大的高并发处理能力,在于其采用了多进程和I/O多路复用的底层实现

  • 进行负载平衡

    所谓负载平衡额就是把大量请求按照我们所指定的方式均衡的分配给集群中的每台服务器

  • 保证后端服务安全

    通过反向代理,隐藏真实的服务器IP地址

  • 热部署

    互联网项目要求7*24小时的进行提供服务,针对这一要求,Nginx提供了热部署功能,即可以在Nginx不停止的情况下,对Nginx进行文件升级,更新配置和更换日志文件。

反向代理

  1. 定义与原理:反向代理是以代理服务器来接受互联网上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给互联网上请求连接的客户端。此时,代理服务器对外就表现为一个服务器。
  2. 主要功能:反向代理的主要功能包括负载均衡(分发流量到多台服务器上,保证服务的可用性和稳定性)、缓存(缓存动态或静态内容,减轻后端服务器的压力,提高页面访问速度)、安全性(隐藏真实的服务器IP地址,提高系统的安全性和隐私性)以及压缩(对页面进行压缩,减少页面的大小,提高页面的加载速度)。

ThreadLocal:

简介

ThreadLocal并不是一个Thread(线程),而是Thread的一个局部变量

ThreadLocal为每一个线程提供单独一份的存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。换句话说,ThreadLocal 为每个线程提供了其自己的变量副本,因此每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。

流程

  1. 创建 ThreadLocal 实例

    使用 ThreadLocal 类,我们首先需要创建一个 ThreadLocal 实例,然后可以通过这个实例来访问线程局部变量。

    1
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
  2. 设置和获取值

    • set(T value): 用于在当前线程中设置线程局部变量的值。
    • get(): 用于获取当前线程中线程局部变量的值。
    1
    2
    threadLocal.set("Thread-A Value");  
    String value = threadLocal.get(); // 在当前线程中,这将返回 "Thread-A Value"
  3. 初始值

    默认情况下,如果没有为 ThreadLocal 设置值,那么调用 get() 方法将返回 null。但我们可以提供一个初始值,通过覆盖 ThreadLocalinitialValue() 方法来实现。

    1
    2
    3
    4
    5
    6
    ThreadLocal<String> threadLocal = new ThreadLocal<String>() {  
    @Override
    protected String initialValue() {
    return "Initial Value";
    }
    };

注意事项

  • ThreadLocal 可能会导致内存泄漏,因为它会在线程的生命周期内保持其值。如果线程长时间运行,并且 ThreadLocal 变量不再需要,但线程没有结束,那么这些变量占用的内存就无法被垃圾回收器回收。因此,在使用完 ThreadLocal 后,最好手动调用 remove() 方法来清除线程局部变量。
  • ThreadLocal 不是解决并发问题的银弹。在大多数情况下,应该优先考虑使用同步机制(如 synchronizedLock)来确保线程安全。ThreadLocal 只是在某些特定场景下提供了一种更优雅的解决方案。

Redis

Redis是一个基于内存的key-value结构数据库,与之对应的mysql是基于磁盘的数据库

由于基于内存存储,所以他的读写性能高,适用于储存热点数据(短时间大量访问数据)如热点商品,资讯,新闻

而mysql这种传统的数据库是基于磁盘IO进行数据读取,随着访问量的增大,这种读取方式劣势明显,于是redis应运而生

优势

  • 性能极高
  • 数据类型丰富,单键值对最大支持512M大小的数据
  • 简单易用,支持所有主流编程语言
  • 支持数据持久化,主从复制,哨兵模式等高可用特性

springBoot整合redis

导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置文件

1
2
3
4
5
6
7
8
spring:
redis:
#Redis服务器地址
host: 192.168.10.3
#端口
port: 6379
#使用几号数据库
database: 0

starter已经给我们提供了两个默认的模板类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}

@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}

我们只需注入StringRedisTemplate来使用模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class SpringBootTestApplicationTests {

@Autowired
StringRedisTemplate template;

@Test
void contextLoads() {
ValueOperations<String, String> operations = template.opsForValue();
operations.set("c", "xxxxx"); //设置值
System.out.println(operations.get("c")); //获取值

template.delete("c"); //删除键
System.out.println(template.hasKey("c")); //判断是否包含键
}

}

WebSocket

简介

WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

过程:客户端向服务端发送一个请求(HandShake)握手,服务端对客户端进行应答(Acknowledgement),这样,客户端与服务端便建立好链接,双方可以进行双向通信。

优势

  • 双向实时通信

    允许在单个,长时间的链接上进行双向实时通信。在需要快速实时更新的应用程序里,比Http更加高效。

  • 降低延迟

    连接一旦建立便会保持开放,数据可以在客户端和服务器之间以比HTTP更低的延迟进行传输

  • 更高效的资源利用

    可以减少重复的请求和响应的开销,因为它的链接只需建立一次。

建立webSocket链接

需要通过HTTP发送一次常规的Get请求,并在请求头中带上Upgrade,告诉服务器,要求从HTTP升级为webSocket,链接便建立成功,之后客户端和服务端就可以进行互相通信。

使用代码

配置类

1
2
3
4
5
6
7
8
9
@Configuration
public class WebSocketConfiguration {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
//一旦ServerEndpointExporter被注册为bean,Spring就会扫描你的应用程序中的@ServerEndpoint注解,并自动配置 和注册相应的WebSocket端点。这使得你能够很容易地将WebSocket集成到Spring应用程序中。
}

WebSocket的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);//存放会话对象
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}

/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);//移除该会话对象
}

/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();//获取所有客户端
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

HTTP

HTTP也是基于TCP的一种协议。请求响应模式,只有客户端向服务端发送请求,服务端才可以响应,不可颠倒,响应过后链接断开,故可称其为短链接

JWT相关总结

(1)令牌的生成:由三部分组成,需要设置令牌的签名算法和签名密钥(保密,只能自己拥有,可随意设置),过期时间,和主题数据(一般为用户id)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);

// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);

return builder.compact();
}

(2) 令牌的校验:即token的解密,通过JWTs的parser方法,设置密钥,以及需要解析的token,如果该过程为出异常,及说明令牌的校验成功

1
2
3
4
5
6
7
8
9
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}

以上两点通常封装在一个util类中,以便登陆时进行校验

(3)登录时所需要的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
@RequestMapping("/admin/employee")
@Slf4j
public class EmployeeController {

@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;

/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);

Employee employee = employeeService.login(employeeLoginDTO);

//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);

EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();

return Result.success(employeeLoginVO);
}

(4)拦截器的设置(interceptor)根据验证jwt来判断是否登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}

//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());

//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}

消息推送的常见方式

(1)轮询:浏览器以指定的时间间隔向服务器发送HTTP请求,服务器实时返回数据给浏览器

缺点:1-由于定时发送请求,可能会导致服务器数据更新,此时浏览器未发送请求,则会造成延迟

​ 2-由于定时要向服务器发送请求,会造成服务器压力过大

(2)长轮询:浏览器发出ajax请求,服务器端收到请求后,会阻塞请求直到有数据或请求超时才返回

对比:由于有等待的过程,所以间隔时间可以长一些,相对于轮询对服务端造成的压力小。

并且由于必须收到数据才可返回,相对于轮询的延迟也会小。

(3)SSE 服务器发送事件

是服务端打开的和客户端之间的一个单向通道。 服务端相应的不再是一次性的数据包,而是text/event-stream类型的数据流信息。 服务器有数据变更时将数据流式传输到客户端。

(4)WebSocket:如上。

补充:全双工:允许数据在两个方向上同时传输。

​ 半双工:允许数据在两个方向上传输,但是同一个时间段内只允许一个方向上传输。

JAVA WebSocket应用由一系列的EndPoint组成。EndPoint是一个java对象,代表WebSocket链接的一端

每一个客户端,服务端都会创建一个EndPoint与之一对一对应,对于服务端,我们可以视为处理具体WebSocket消息的接口

EndPoint实例在WebSocket握手时创建,并在客户端与服务端连接过程中有效,最后在连接关闭时结束

服务端API

发送消息:1-服务端接受客户端发送的数据 常用:注解式@OnMessage

​ 2-服务端推送数据给客户端:由RemoteEndpoint完成,实例由Session维护

​ 通过session.getBasicRemote获取同步消息发送的实例,调用sendXXX()发送消息

​ 通过session.getAsyncRemote获取异步消息发送的实例,调用sendXXX()发送消息

AOP:面向切面

动态代理是面向切面编程最主流的实现。而springAOP是spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。对特定的方法功能进行加强或改变其功能。

好处:代码无侵入,减少重复代码,提高开发效率,维护方便

核心概念

(1)连接点:joinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

通过joinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名,方法名,方法参数。

joinPoint可获得方法的签名,而方法签名包含了方法的名称,返回类型及参数列表

(2)通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)

(3)切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法被执行时被应用,通常通过切点表达式实现其功能。

区别:连接点时程序中所有潜在的可插入通知的位置,切入点则是从这些连接点筛选出来的,实际要附加通知的具体位置

(4)切面:Aspect,描述通知与切入点的对应关系(通知加切入点)

(5)目标对象:Target,通知所应用的对象

切点表达式(带?可省略)execution(访问修饰符?返回值 包名.类名.?方法名(方法参数)throws 异常?)

动态代理

动态代理是在运行时动态的为对象创建代理的技术

必须实现接口 支持拦截public方法 支持拦截protected方法 拦截默认作用域方法
JDK动态代理
CGLIB代理

虽然CGLIB代理支持拦截非public作用域方法调用,但不同对象交互是,建议还是以public方法调用为主

文件上传(阿里云OSS)

使用流程

前往阿里云官方完成注册等流程,新建一个bucket,获取到如下四个数据 access-key-id access-key-secret endpoint bucket-name

写在配置文件中

1
2
3
4
5
alioss:
access-key-id:
access-key-secret:
endpoint:
bucket-name:

后端相应的接口

  • 接受上传来的文件
  • 将文件存储起来(OSS)
  • 返回文件的url
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/admin/common")
public class CommonController {
@Resource
AliOssUtil aliOssUtil;

@PostMapping("/upload")
public Result<String>upload(MultipartFile file){
try{
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String ObjectName = UUID.randomUUID().toString()+extension;
String filePath = aliOssUtil.upload(file.getBytes(), ObjectName);
return Result.success(filePath);
}catch (IOException e){
throw new RuntimeException(e);
}
}
}

其中所用到的工具类(有阿里云官方代码改造而来)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

//需要为自己获取到的
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;

/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}

//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);

log.info("文件上传到:{}", stringBuilder.toString());

return stringBuilder.toString();
}
}