Odoo事件驱动架构:WebSocket实时通讯与消息队列实现
架构概览
Odoo采用事件驱动架构实现实时通讯,核心通过WebSocket协议和消息队列机制处理跨模块、跨客户端的实时数据交换。该架构主要包含以下组件:
- 消息总线:addons/bus/models/bus.py实现消息的存储与分发
- WebSocket服务:addons/bus/static/src/services/bus_service.js处理客户端连接
- 消息队列:基于PostgreSQL的NOTIFY/LISTEN机制实现异步消息传递
- 多标签同步:addons/bus/static/src/multi_tab_service.js确保多标签页状态一致性
WebSocket连接建立流程
Odoo的实时通讯从客户端WebSocket连接建立开始,完整流程如下:
关键实现代码位于addons/bus/static/src/services/bus_service.js的ensureWorkerStarted函数,负责创建WebSocket连接并进行身份验证:
async function ensureWorkerStarted() {
if (!connectionInitializedDeferred) {
connectionInitializedDeferred = new Deferred();
let uid = Array.isArray(session.user_id) ? session.user_id[0] : user.userId;
await workerService.ensureWorkerStarted();
await workerService.registerHandler(handleMessage);
workerService.send("BUS:INITIALIZE_CONNECTION", {
websocketURL: `${params.serverURL.replace("http", "ws")}/websocket?version=${session.websocket_worker_version}`,
db: session.db,
debug: odoo.debug,
lastNotificationId: legacyMultiTab.getSharedValue("last_notification_id", 0),
uid,
startTs: startedAt.valueOf(),
});
}
await connectionInitializedDeferred;
}
消息队列实现机制
Odoo利用PostgreSQL的NOTIFY/LISTEN机制实现轻量级消息队列,避免了独立消息队列服务的部署复杂性。核心实现位于addons/bus/models/bus.py:
消息发送流程
- 应用模块调用
bus.bus._sendone发送消息 - 消息被存储到
bus_bus表中 - 事务提交后触发PostgreSQL NOTIFY [addons/bus/models/bus.py#L148]
@self.env.cr.postcommit.add
def notify():
payloads = get_notify_payloads(
list(self.env.cr.postcommit.data.pop("bus.bus.channels"))
)
with odoo.sql_db.db_connect("postgres").cursor() as cr:
for payload in payloads:
cr.execute(
SQL(
"SELECT %s('imbus', %s)",
SQL.identifier(ODOO_NOTIFY_FUNCTION),
payload,
)
)
消息接收与分发
服务器通过持续监听PostgreSQL的imbus频道接收消息,并分发给相应的WebSocket连接:
def loop(self):
""" Dispatch postgres notifications to the relevant websockets """
_logger.info("Bus.loop listen imbus on db postgres")
with odoo.sql_db.db_connect('postgres').cursor() as cr, \
selectors.DefaultSelector() as sel:
cr.execute("listen imbus")
cr.commit()
conn = cr._cnx
sel.register(conn, selectors.EVENT_READ)
while not stop_event.is_set():
if sel.select(TIMEOUT):
conn.poll()
channels = []
while conn.notifies:
channels.extend(json.loads(conn.notifies.pop().payload))
# relay notifications to websockets
websockets = set()
for channel in channels:
websockets.update(self._channels_to_ws.get(hashable(channel), []))
for websocket in websockets:
websocket.trigger_notification_dispatching()
消息总线核心实现
消息总线是Odoo事件驱动架构的核心,负责消息的存储、过滤和分发。主要功能包括:
1. 消息存储与清理
addons/bus/models/bus.py中定义的bus.bus模型负责消息的持久化存储:
class BusBus(models.Model):
_name = 'bus.bus'
_description = 'Communication Bus'
channel = fields.Char('Channel')
message = fields.Char('Message')
@api.autovacuum
def _gc_messages(self):
gc_retention_seconds = int(
self.env["ir.config_parameter"]
.sudo()
.get_param("bus.gc_retention_seconds", DEFAULT_GC_RETENTION_SECONDS)
)
timeout_ago = fields.Datetime.now() - datetime.timedelta(seconds=gc_retention_seconds)
# 直接SQL删除过期消息,提高效率
self.env.cr.execute("DELETE FROM bus_bus WHERE create_date < %s", (timeout_ago,))
2. 消息发送API
_sendone方法提供了发送消息的标准接口,支持事务提交后异步发送:
@api.model
def _sendone(self, target, notification_type, message):
"""Low-level method to send notification to target"""
self._ensure_hooks()
channel = channel_with_db(self.env.cr.dbname, target)
self.env.cr.precommit.data["bus.bus.values"].append({
"channel": json_dump(channel),
"message": json_dump({
"type": notification_type,
"payload": message,
})
})
self.env.cr.postcommit.data["bus.bus.channels"].add(channel)
3. 消息订阅与过滤
客户端通过_poll方法订阅指定频道的消息,支持基于时间戳和消息ID的过滤:
@api.model
def _poll(self, channels, last=0, ignore_ids=None):
# first poll return the notification in the 'buffer'
if last == 0:
timeout_ago = fields.Datetime.now() - datetime.timedelta(seconds=TIMEOUT)
domain = [('create_date', '>', timeout_ago)]
else: # else returns the unread notifications
domain = [('id', '>', last)]
if ignore_ids:
domain.append(("id", "not in", ignore_ids))
channels = [json_dump(channel_with_db(self.env.cr.dbname, c)) for c in channels]
domain.append(('channel', 'in', channels))
notifications = self.sudo().search_read(domain, ["message"])
# format and return notifications
result = []
for notif in notifications:
result.append({
'id': notif['id'],
'message': json.loads(notif['message']),
})
return result
客户端消息处理
客户端接收消息后,通过事件总线分发给相应的处理函数。addons/bus/static/src/services/bus_service.js中的消息处理流程如下:
function handleMessage(messageEv) {
const { type, data } = messageEv.data;
switch (type) {
case "BUS:NOTIFICATION": {
const notifications = data.map(({ id, message }) => ({ id, ...message }));
state.lastNotificationId = notifications.at(-1).id;
legacyMultiTab.setSharedValue("last_notification_id", state.lastNotificationId);
for (const { id, type, payload } of notifications) {
notificationBus.trigger(type, { id, payload });
busService._onMessage(env, id, type, payload);
}
break;
}
// 其他消息类型处理...
}
}
客户端可以通过订阅特定事件类型来接收消息:
// 订阅通知
busService.subscribe('mail.notification', (payload) => {
console.log('收到新邮件通知:', payload);
renderNotification(payload);
});
多标签页同步机制
Odoo通过共享Worker和本地存储实现多标签页间的状态同步,核心实现位于addons/bus/static/src/multi_tab_shared_worker_service.js。
同步流程如下:
- 主标签页建立WebSocket连接
- 其他标签页通过SharedWorker共享连接
- 使用localStorage同步最后消息ID
// 多标签同步实现
legacyMultiTab.setSharedValue("last_notification_id", state.lastNotificationId);
// 从共享存储获取最后消息ID
const lastId = legacyMultiTab.getSharedValue("last_notification_id", 0);
性能优化策略
Odoo的事件驱动架构采用多种优化策略确保高并发场景下的性能:
1. 消息负载拆分
当消息负载过大时,自动拆分消息以适应PostgreSQL NOTIFY的 payload 限制:
def get_notify_payloads(channels):
"""Generates json payloads for imbus NOTIFY, splitting large payloads"""
if not channels:
return []
payload = json_dump(channels)
if len(channels) == 1 or len(payload.encode()) < NOTIFY_PAYLOAD_MAX_LENGTH:
return [payload]
else:
pivot = math.ceil(len(channels) / 2)
return (get_notify_payloads(channels[:pivot]) +
get_notify_payloads(channels[pivot:]))
2. 消息过期清理
自动清理过期消息,避免存储膨胀:
@api.autovacuum
def _gc_messages(self):
gc_retention_seconds = int(
self.env["ir.config_parameter"]
.sudo()
.get_param("bus.gc_retention_seconds", DEFAULT_GC_RETENTION_SECONDS)
)
timeout_ago = fields.Datetime.now() - datetime.timedelta(seconds=gc_retention_seconds)
self.env.cr.execute("DELETE FROM bus_bus WHERE create_date < %s", (timeout_ago,))
3. 连接池管理
通过连接池复用数据库连接,减少连接建立开销:
with odoo.sql_db.db_connect('postgres').cursor() as cr, \
selectors.DefaultSelector() as sel:
cr.execute("listen imbus")
cr.commit()
conn = cr._cnx
sel.register(conn, selectors.EVENT_READ)
实际应用场景
Odoo的事件驱动架构在多个模块中得到应用,典型场景包括:
1. 实时通知系统
addons/mail/模块使用消息总线实现邮件和通知的实时推送:
# 发送邮件通知
self.env['bus.bus']._sendone(
partner.id,
'mail.notification',
{'message_id': message.id, 'title': message.subject}
)
2. 库存变动实时更新
addons/stock/模块通过消息总线实时推送库存变动:
# 库存变动通知
self.env['bus.bus']._sendone(
'stock_channel',
'stock.quantity_change',
{'product_id': product.id, 'new_quantity': product.qty_available}
)
3. 多用户协作编辑
addons/document/模块利用WebSocket实现文档的实时协作编辑。
部署与扩展建议
在生产环境部署Odoo的事件驱动架构时,建议考虑以下几点:
- 数据库优化:确保PostgreSQL配置足够的连接数和内存
- 负载均衡:使用Redis等外部消息队列替代PostgreSQL NOTIFY以支持更大规模部署
- 监控:通过addons/bus/static/src/debug/工具监控消息总线状态
- 水平扩展:多服务器部署时确保WebSocket连接粘性会话(sticky session)
总结
Odoo的事件驱动架构通过WebSocket和基于PostgreSQL的消息队列,构建了高效的实时通讯系统。该架构具有以下特点:
- 松耦合:通过事件总线解耦系统组件,提高可扩展性
- 实时性:WebSocket协议确保消息低延迟传递
- 可靠性:消息持久化和重试机制保证消息不丢失
- 高效性:多种优化策略确保高并发场景下的系统性能
核心实现代码分布在addons/bus/models/bus.py和addons/bus/static/src/目录下,通过这些组件的协同工作,Odoo实现了跨模块、跨客户端的实时数据交换能力。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



