为何在PostgreSQL中同时使用乐观并发与咨询锁?
我是PostgreSQL新手,从Eventide项目的Message Store数据库中获取了如下write_message函数:
CREATE OR REPLACE FUNCTION write_message( id varchar, stream_name varchar, "type" varchar, data jsonb, metadata jsonb DEFAULT NULL, expected_version bigint DEFAULT NULL ) RETURNS bigint AS $$ DECLARE _message_id uuid; _stream_version bigint; _next_position bigint; BEGIN PERFORM acquire_lock(write_message.stream_name); _stream_version := stream_version(write_message.stream_name); IF _stream_version IS NULL THEN _stream_version := -1; END IF; IF write_message.expected_version IS NOT NULL THEN IF write_message.expected_version != _stream_version THEN RAISE EXCEPTION 'Wrong expected version: % (Stream: %, Stream Version: %)', write_message.expected_version, write_message.stream_name, _stream_version; END IF; END IF; _next_position := _stream_version + 1; _message_id = uuid(write_message.id); INSERT INTO messages ( id, stream_name, position, type, data, metadata ) VALUES ( _message_id, write_message.stream_name, _next_position, write_message.type, write_message.data, write_message.metadata ) ; IF current_setting('debug_write', true) = 'on' OR current_setting('debug', true) = 'on' THEN RAISE NOTICE '» write_message'; RAISE NOTICE 'id ($1): %', write_message.id; RAISE NOTICE 'stream_name ($2): %', write_message.stream_name; RAISE NOTICE 'type ($3): %', write_message.type; RAISE NOTICE 'data ($4): %', write_message.data; RAISE NOTICE 'metadata ($5): %', write_message.metadata; RAISE NOTICE 'expected_version ($6): %', write_message.expected_version; RAISE NOTICE '_stream_version: %', _stream_version; RAISE NOTICE '_next_position: %', _next_position; END IF; RETURN _next_position; END; $$ LANGUAGE plpgsql VOLATILE;
该函数对write_message.stream_name参数使用咨询锁,同时基于stream_version(write_message.stream_name)实现了乐观并发控制——stream_version本质是查询与传入参数同名的stream_name下messages表的max(position),position是同stream_name下记录的递增序号。若函数执行期间有其他同stream_name的消息写入,传入的expected_version会小于当前的max(position)。
我的问题是:为何要同时对同一对象使用类似操作系统信号量(如Windows临界区/.NET的Monitor.Enter)的咨询锁与乐观并发这两种机制?
以下是acquire_lock函数的定义:
CREATE OR REPLACE FUNCTION acquire_lock( stream_name varchar ) RETURNS bigint AS $$ DECLARE _category varchar; _category_name_hash bigint; BEGIN _category := category(acquire_lock.stream_name); _category_name_hash := hash_64(_category); PERFORM pg_advisory_xact_lock(_category_name_hash); IF current_setting('debug_write', true) = 'on' OR current_setting('debug', true) = 'on' THEN RAISE NOTICE '» acquire_lock'; RAISE NOTICE 'stream_name: %', acquire_lock.stream_name; RAISE NOTICE '_category: %', _category; RAISE NOTICE '_category_name_hash: %', _category_name_hash; END IF; RETURN _category_name_hash; END; $$ LANGUAGE plpgsql VOLATILE;
以下是stream_version函数的定义:
CREATE OR REPLACE FUNCTION message_store.stream_version( stream_name varchar ) RETURNS bigint AS $$ DECLARE _stream_version bigint; BEGIN SELECT max(position) into _stream_version FROM messages WHERE messages.stream_name = stream_version.stream_name; RETURN _stream_version; END; $$ LANGUAGE plpgsql VOLATILE;
解答
这两种机制是互补的,分别解决不同层面的问题:
咨询锁的作用:减少高并发下的乐观锁冲突重试
从acquire_lock的实现可以看到,它并不是给单个流加锁,而是给流所属的**分类(category)**加事务级咨询锁。这样同一分类下的所有流写入会被串行化,避免同一分类下大量流同时写入时,频繁触发乐观锁的版本冲突(因为每个流的写入都要查max(position),高并发下会出现大量“预期版本不符”的异常,导致客户端不断重试)。通过分类锁把同一组相关流的写入串行化,能有效降低冲突率,提升整体吞吐量。乐观并发控制的作用:保证单个流的版本一致性
乐观锁的expected_version检查是为了确保客户端是基于自己读取到的最新流版本进行写入。假设客户端先查询了流版本,然后等待咨询锁的释放,在等待期间可能已经有其他客户端写入了该流的新消息,如果没有乐观锁检查,当前客户端就会基于旧版本写入,破坏流的顺序性和一致性——这正是事件溯源模式中绝对不允许的情况。乐观锁的检查能最终确保写入操作符合客户端预期的流状态。两者结合的必要性
如果只使用乐观锁:高并发场景下,同一分类的多个流写入会导致大量版本冲突,客户端重试次数激增,系统性能下降;如果只使用咨询锁:虽然避免了冲突,但客户端可能基于旧的流版本写入(比如查询版本后等待锁的间隙,流已经被修改),无法保证业务层面的一致性。
两者搭配使用,既通过分类级的咨询锁降低了冲突概率,又通过单流的乐观锁检查保障了最终的业务正确性,是针对事件溯源场景高并发写入的一种优化方案。
内容的提问来源于stack exchange,提问作者nausaf

