苍穹外卖-DAY10

一、springtask

image-20260208134110219

1.corn表达式

corn表达式在线生成网址

1
cron.qqe2.com

Java (Spring/Quartz):6 或 7 个字段 (秒 分 时 日 月 周 [年])

为了通用性,我们以最完整的 6-7 字段结构为例(这也是后端开发最常遇到的):

1
2
秒    分    时    日    月    周    年(可选)
* * * * * * *

字段含义表

位置 字段含义 允许的值 允许的特殊字符
1 (Seconds) 0-59 , - * /
2 (Minutes) 0-59 , - * /
3 (Hours) 0-23 , - * /
4 (Day of Month) 1-31 , - * / ? L W
5 (Month) 1-12 或 JAN-DEC , - * /
6 (Day of Week) 1-7 或 SUN-SAT , - * / ? L #
7 (Year) 留空 或 1970-2099 , - * /

注意: 在 Linux crontab 中,没有“秒”这一位,第一位是“分”。

  • 如果在“周”写了 *,意思是“不管今天是星期几(哪怕是周一到周日每一天)”。
  • 如果在“周”写了 ?,意思是“我不关心星期几,因为我已经指定了具体的日期(比如每月10号)”。
  • 规则:Day 和 Week 字段,通常必有一个是 ?

2.springtask

image-20260208134712613

自定义任务类

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
@Controller
@Slf4j
public class OrderTask {

@Autowired
private OrderService orderService;

@Autowired
private OrderMapper orderMapper;


@Scheduled(cron = "0 * * * * ?")
public void processOrders(){
log.info("定时处理超时订单:{}", LocalDateTime.now());

// 获取小于当前时间15分钟前的超时订单
List<Orders> timeoutOrders = orderService.getStatusAndOrderTimeL(Orders.PENDING_PAYMENT,LocalDateTime.now().plusMinutes(-15));

if(timeoutOrders != null && !timeoutOrders.isEmpty()){
for(Orders order : timeoutOrders){
order.setStatus(Orders.CANCELLED);
order.setCancelReason("订单超时,系统自动取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
}
}
}

@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrders(){
log.info("定时处理超时未确认收货订单:{}", LocalDateTime.now());

LocalDateTime time = LocalDateTime.now().plusMinutes(-60);

List<Orders> ordersList = orderService.getStatusAndOrderTimeL(Orders.DELIVERY_IN_PROGRESS,time);

if(ordersList != null && !ordersList.isEmpty()){
for(Orders order : ordersList){
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
}
}


}
}

二、WebSocket

1.Socket网络协议

image-20260208144304736

简单来说,WebSocket 解决了 HTTP 协议的一个最大痛点:服务器无法主动向客户端(浏览器)发消息。

2. 对比

  • HTTP (传统的 Web):像 “发邮件”
    • 你需要在这个页面看到新消息,你必须手动刷新(或者用 JS 定时去问服务器)。
    • 流程:你问服务器 -> 服务器回你 -> 挂断
    • 服务器:“你不问我,我就不理你。”
  • WebSocket (实时 Web):像 “打电话”
    • 一旦电话接通(建立连接),你们俩谁都可以随时说话,不需要每次都重新拨号。
    • 流程:你拨通服务器 -> 连接保持 -> 你说 -> 服务器说 -> 服务器再说 -> 你说…
    • 服务器:“我有新消息,直接推给你,不用你问。”

2. WebSocket

在没有 WebSocket 之前,如果你想做一个“即时聊天”或者“股票实时大盘”,你只能用 轮询 (Polling)

前端 JS:老大,有新消息吗?

后端:没有。

(过了 1 秒)

前端 JS:老大,有新消息吗?

后端:没有。

(过了 1 秒)

前端 JS:老大,有新消息吗?

后端:有!

这种方式的缺点

  1. 浪费带宽:每次请求都要带一堆 HTTP Header,有效数据可能就几个字。
  2. 延迟高:消息可能在两次轮询的中间到了,但必须等下次询问才能拿到。
  3. 服务器累:一万个用户每秒问一次,服务器 CPU 直接爆炸。

WebSocket 完美解决了这个问题:它实现了 全双工通信 (Full-Duplex)。连接一旦建立,服务器有数据就直接推过来,几乎没有延迟。

3. 建立连接握手协议

WebSocket 并不是一个全新的协议,它其实是 借用了 HTTP 来“搭桥”

过程如下:

  1. 发起请求:浏览器发一个标准的 HTTP 请求给服务器,但是 Header 里带了特殊的暗号:

    1
    2
    3
    4
    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    • Upgrade: websocket:意思是“大哥,我想升级协议,咱别用 HTTP 了,改用 WebSocket 吧”。
  2. 服务器响应:如果服务器支持,就会返回 101 状态码:

    1
    2
    3
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    • 101 Switching Protocols:意思是“收到,协议切换成功”。
  3. 连接建立:哪怕这个 HTTP 请求结束了,底层的 TCP 连接不会断开。这就是一条全双工的通道了。此后传输的数据不再是 HTTP 报文,而是 WebSocket 的“数据帧”。

4. 核心注意事项

  1. 心跳检测 (Heartbeat)

    网络是不稳定的。如果网线被拔了,服务器可能不知道客户端断了,还傻傻地拿着连接。所以通常客户端每隔 30 秒要发一个“Ping”,服务器回一个“Pong”,证明咱们还连着。

  2. 负载均衡 (Session 共享)

    如果你的后端有 2 台服务器(集群),用户 A 连上了服务器 1,但支付回调打到了服务器 2。服务器 2 手里没有用户 A 的 WebSocket 连接,怎么推?

    • 解决:需要引入 Redis 的 Pub/Sub(发布订阅)或者消息队列,让服务器 2 通知服务器 1 去推送。

5.springboot实现过程

image-20260208144956493

二、ConcurrentHashMap

ConcurrentHashMap

简单来说,ConcurrentHashMap 是 Java 中专门用于 多线程高并发场景 下的 Map(键值对集合)实现。

为了让你完全理解它的作用,我们需要对比一下你原本使用的 HashMap 和现在的 ConcurrentHashMap

1. HashMap 会报错

在 WebSocket 服务器中,sessionMap 的作用是存储所有当前在线用户的连接对象(Session)。

  • 场景: 想象有 100 个用户同时点击“连接”,还有 50 个用户同时“断开连接”或发送消息。
  • HashMap 的弱点: HashMap线程不安全的。它就像一个没有锁的公共记事本。
    • 如果 线程 A 正在往本子上写名字(有人上线),同时 线程 B 正在撕掉某一页(有人下线),或者 线程 C 正在读取名单。
    • 后果: HashMap 的内部结构会乱套,导致数据覆盖、丢失,或者直接抛出 ConcurrentModificationException(并发修改异常),甚至在某些旧版本 Java 中会导致 CPU 100% 死循环。

2. ConcurrentHashMap

ConcurrentHashMap 就像是一个管理严格的档案室。它专为多线程设计,核心作用如下:

  • 线程安全 (Thread Safety): 它保证了不管有多少个线程同时读写,数据内部结构都不会乱。你不需要自己写 synchronized 锁,它内部已经处理好了。
  • 高并发性能 (High Concurrency):
    • 以前的笨办法 (Hashtable): 相当于把整个档案室锁住,一个人进去操作,其他人都在门口排队。虽然安全,但效率极低。
    • ConcurrentHashMap 的聪明办法: 它采用了分段锁(Java 7)或 CAS + 节点锁(Java 8+)机制。
    • 通俗解释: 它只锁住你正在操作的那一行柜子,而不是锁住整个房间。当你在操作“A”开头的用户时,别人完全可以同时操作“Z”开头的用户,互不干扰。

3. 核心差异对比表

特性 HashMap (你的旧代码) ConcurrentHashMap (修复后的代码)
线程安全 不安全 (多线程必崩) 安全 (专为多线程设计)
性能 单线程极快,多线程无法工作 多线程下极高,单线程略慢(可忽略)
允许 Null key 和 value 都可以是 null key 和 value 都不允许是 null (注意点!)
适用场景 局部变量、单线程处理 全局缓存、WebSocket Session管理、共享资源

4. 代码场景还原

在你的 WebSocketServer 中,实际发生的事情是这样的:

使用 ConcurrentHashMap 后:

1
2
3
4
5
6
7
8
9
10
11
12
// 当用户上线 (Thread 1)
sessionMap.put(userId, session); // 内部会自动加局部锁,安全写入

// 当用户下线 (Thread 2)
sessionMap.remove(userId); // 即使Thread 1正在写,Thread 2也能安全移除

// 给所有人发消息 (Thread 3)
for (String key : sessionMap.keySet()) {
// 即使遍历过程中有人上线/下线,这里也不会报错!
// (HashMap在这里如果遇到有人上下线,就会抛出异常)
sessionMap.get(key).getBasicRemote().sendText(message);
}

总结

这个修改将 sessionMap 从一个普通的非线程安全容器变成了一个线程安全的并发容器

这是编写 WebSocket 服务端的标准操作。如果不改,你的服务在只有 1 个用户时是正常的,一旦有 2 个以上用户并发操作,服务就会随机崩溃。