You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PostgreSQL 14.5 PL/pgSQL函数并发执行时出现tuple concurrently updated错误

并发场景下PL/pgSQL权限校验函数抛出tuple concurrently updated错误排查

我编写了一个用作权限校验中间件的PL/pgSQL函数,单例执行完全正常,但在并发测试场景(20个虚拟用户1分钟内发起350-400次请求)下,会随机抛出"original: error: tuple concurrently updated"错误。函数逻辑为:通过多表关联查询判断用户归属,返回1或0;若为1则执行另一查询并返回JSON数据,否则返回指定JSON。尝试在函数中添加commit语句后问题仍未解决,以下是相关代码及错误信息,请求协助排查。

PL/pgSQL函数代码

create or replace function getUserData() returns json as 
$body$
    declare 
        v_belongs_to_group json;
        v_has_permission_in_group json;
    begin
        if (
            select case when exists(
                SELECT u.email , utg.id, g.group_name, g.id 
                    FROM users as u
                    inner join public.users_to_groups as utg on utg.user_id = u.id
                    inner join "groups" as g on g.id = utg.group_id 
                    where u.email='email@email.com' 
                    and g.group_name ='necessary group'
                    and g.status_id = 1
                ) then cast(1 as integer) else cast(0 as integer) end
        ) = 1 then
                select 
                    jsonb_build_object(
                        'user_id', u.id
                    )
                    from users as u 
                    inner join users_to_groups utg on utg.user_id=u.id 
                    inner join "groups" g on g.id = utg.group_id
                    inner join users_to_permissions utp on utp.user_group_id = utg.id 
                    inner join permissions p on p.id =utp.permission_id 
                    where u.email= 'email@email.com' 
                    and g.group_name = 'another Group' 
                    and p."name" = 'some permission'
                    and g.status_id = 1
                    into v_has_permission_in_group;
                return to_json(v_has_permission_in_group);
            else
                v_belongs_to_group = jsonb_build_object('v_belongs_to_group',false);
                return to_json(v_belongs_to_group);
        end if;
    commit;
end; 
$body$ 
language plpgsql;

select * from getUserData() as fruit;

完整错误栈

DatabaseError [SequelizeDatabaseError]: tuple concurrently updated
        at Query.formatError (src\node_modules\sequelize\lib\dialects\postgres\query.js:366:16)
        at src\node_modules\sequelize\lib\dialects\postgres\query.js:72:18  
        at tryCatcher (src\node_modules\bluebird\js\release\util.js:16:23)  
        at Promise._settlePromiseFromHandler (src\node_modules\bluebird\js\release\promise.js:547:31)
        at Promise._settlePromise (src\node_modules\bluebird\js\release\promise.js:604:18)
        at Promise._settlePromise0 (src\node_modules\bluebird\js\release\promise.js:649:10)
        at Promise._settlePromises (src\node_modules\bluebird\js\release\promise.js:725:18)
        at _drainQueueStep (src\node_modules\bluebird\js\release\async.js:93:12)
        at _drainQueue (src\node_modules\bluebird\js\release\async.js:86:9) 
        at Async._drainQueues (src\node_modules\bluebird\js\release\async.js:102:5)
        at Async.drainQueues (src\node_modules\bluebird\js\release\async.js:15:14)
        at process.processImmediate (node:internal/timers:483:21) {
      parent: error: tuple concurrently updated
          at Parser.parseErrorMessage (src\node_modules\pg-protocol\dist\parser.js:287:98)
          at Parser.handlePacket (src\node_modules\pg-protocol\dist\parser.js:126:29)
          at Parser.parse (src\node_modules\pg-protocol\dist\parser.js:39:38)
          at Socket.<anonymous> (src\node_modules\pg-protocol\dist\index.js:11:42)
          at Socket.emit (node:events:520:28)
          at addChunk (node:internal/streams/readable:559:12)
          at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
          at Readable.push (node:internal/streams/readable:390:5)
          at TCP.onStreamRead (node:internal/stream_base_commons:191:23) {
        length: 90,
        severity: 'ERROR',
        code: 'XX000',
        detail: undefined,
        hint: undefined,
        position: undefined,
        internalPosition: undefined,
        internalQuery: undefined,
        where: undefined,
        schema: undefined,
        table: undefined,
        column: undefined,
        dataType: undefined,
        constraint: undefined,
        file: 'heapam.c',
        line: '4260',
        routine: 'simple_heap_update',
        sql: 'create or replace function getUserData() returns json as \n' +
          '        $body$\n' +
          '          declare \n' +
          '            belongs_to_default_group json;\n' +
          '            has_permission_in_group json;\n' +
          '          begin\n' +
          '            if (\n' +
          '              select case when exists(\n' +
          '                SELECT u.email , utg.id, g.group_name, g.id \n' +
          '                  FROM users as u\n' +
          '                  inner join public.users_to_groups as utg on utg.user_id = u.id\n' +
          '                  inner join "groups" as g on g.id = utg.group_id \n' +
          '                  where u.email=\'email@email.com\' \n' +
          '                  and g.group_name =\'necessary group\'\n' +
          '                  and g.status_id = 1 for share\n' +
          '                ) then cast(1 as integer) else cast(0 as integer) end\n' +
          '            ) = 1 then\n' +
          '                    select \n' +
          '                      jsonb_build_object(\n' +
          '                        \'user_id\', u.id\n' +
          '                      )\n' +
          '                      from users as u \n' +
          '                      inner join users_to_groups utg on utg.user_id=u.id \n' +
          '                      inner join "groups" g on g.id = utg.group_id\n' +
          '                      inner join users_to_permissions utp on utp.user_group_id = utg.id \n' +
          '                      inner join permissions p on p.id =utp.permission_id \n' +
          '                      where u.email= \'email@email.com\' \n' +
          '                      and g.group_name =\'another Group\' \n' +
          '                      and p."name" = \'some permission\'\n' +
          '                      and g.status_id = 1\n' +
          '                      into has_permission_in_group for share;\n' +
          '                    return to_json(has_permission_in_group);\n' +
          '                else\n' +
          '                    belongs_to_default_group = jsonb_build_object(\'belongs_to_default_group\',false);\n' +
          '                    return to_json(belongs_to_default_group);\n' +
          '            end if;\n' +
          '            commit;\n' +
          '        end; \n' +
          '        $body$ \n' +
          '        language plpgsql;\n' +
          '        \n' +
          '        select * from getUserData() as fruit;',
        parameters: undefined
      },
      original: error: tuple concurrently updated
          at Parser.parseErrorMessage (src\node_modules\pg-protocol\dist\parser.js:287:98)
          at Parser.handlePacket (src\node_modules\pg-protocol\dist\parser.js:126:29)
          at Parser.parse (src\node_modules\pg-protocol\dist\parser.js:39:38)
          at Socket.<anonymous> (src\node_modules\pg-protocol\dist\index.js:11:42)
          at Socket.emit (node:events:520:28)
          at addChunk (node:internal/streams/readable:559:12)
          at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
          at Readable.push (node:internal/streams/readable:390:5)
          at TCP.onStreamRead (node:internal/stream_base_commons:191:23) {
        length: 90,
        severity: 'ERROR',
        code: 'XX000',
        detail: undefined,
        hint: undefined,
        position: undefined,
        internalPosition: undefined,
        internalQuery: undefined,
        where: undefined,
        schema: undefined,
        table: undefined,
        column: undefined,
        dataType: undefined,
        constraint: undefined,
        file: 'heapam.c',
        line: '4260',
        routine: 'simple_heap_update',
        sql: 'create or replace function getUserData() returns json as \n' +
          '        $body$\n' +
          '          declare \n' +
          '            belongs_to_default_group json;\n' +
          '            has_permission_in_group json;\n' +
          '          begin\n' +
          '            if (\n' +
          '              select case when exists(\n' +
          '                SELECT u.email , utg.id, g.group_name, g.id \n' +
          '                  FROM users as u\n' +
          '                  inner join public.users_to_groups as utg on utg.user_id = u.id\n' +
          '                  inner join "groups" as g on g.id = utg.group_id \n' +
          '                  where u.email=\'email@email.com\' \n' +
          '                  and g.group_name =\'necessary group\'\n' +
          '                  and g.status_id = 1 for share\n' +
          '                ) then cast(1 as integer) else cast(0 as integer) end\n' +
          '            ) = 1 then\n' +
          '                    select \n' +
          '                      jsonb_build_object(\n' +
          '                        \'user_id\', u.id\n' +
          '                      )\n' +
          '                      from users as u \n' +
          '                      inner join users_to_groups utg on utg.user_id=u.id \n' +
          '                      inner join "groups" g on g.id = utg.group_id\n' +
          '                      inner join users_to_permissions utp on utp.user_group_id = utg.id \n' +
          '                      inner join permissions p on p.id =utp.permission_id \n' +
          '                      where u.email= \'email@email.com\' \n' +
          '                      and g.group_name =\'another Group\' \n' +
          '                      and p."name" = \'some permission\'\n' +
          '                      and g.status_id = 1\n' +
          '                      into has_permission_in_group for share;\n' +
          '                    return to_json(has_permission_in_group);\n' +
          '                else\n' +
          '                    belongs_to_default_group = jsonb_build_object(\'belongs_to_default_group\',false);\n' +
          '                    return to_json(belongs_to_default_group);\n' +
          '            end if;\n' +
          '            commit;\n' +
          '        end; \n' +
          '        $body$ \n' +
          '        language plpgsql;\n' +
          '        \n' +
          '        select * from getUserData() as fruit;',
        parameters: undefined
      },
      sql: 'create or replace function getUserData() returns json as \n' +
        '        $body$\n' +
        '          declare \n' +
        '            belongs_to_default_group json;\n' +
        '            has_permission_in_group json;\n' +
        '          begin\n' +
        '            if (\n' +
        '              select case when exists(\n' +
        '                SELECT u.email , utg.id, g.group_name, g.id \n' +
        '                  FROM users as u\n' +
        '                  inner join public.users_to_groups as utg on utg.user_id = u.id\n' +
        '                  inner join "groups" as g on g.id = utg.group_id \n' +
        '                  where u.email=\'email@email.com\' \n' +
        '                  and g.group_name =\'necessary group\'\n' +
        '                  and g.status_id = 1 for share\n' +
        '                ) then cast(1 as integer) else cast(0 as integer) end\n' +
        '            ) = 1 then\n' +
        '                    select \n' +
        '                      jsonb_build_object(\n' +
        '                        \'user_id\', u.id\n' +
        '                      )\n' +
        '                      from users as u \n' +
        '                      inner join users_to_groups utg on utg.user_id=u.id \n' +
        '                      inner join "groups" g on g.id = utg.group_id\n' +
        '                      inner join users_to_permissions utp on utp.user_group_id = utg.id \n' +
        '                      inner join permissions p on p.id =utp.permission_id \n' +
        '                      where u.email= \'email@email.com\' \n' +
        '                      and g.group_name =\'another Group\' \n' +
        '                      and p."name" = \'some permission\'\n' +
        '                      and g.status_id = 1\n' +
        '                      into has_permission_in_group for share;\n' +
        '                    return to_json(has_permission_in_group);\n' +
        '                else\n' +
        '                    belongs_to_default_group = jsonb_build_object(\'belongs_to_default_group\',false);\n' +
        '                    return to_json(belongs_to_default_group);\n' +
        '            end if;\n' +
        '            commit;\n' +
        '        end; \n' +
        '        $body$ \n' +
        '        language plpgsql;\n' +
        '        \n' +
        '        select * from getUserData() as fruit;',
      parameters: undefined
    }

Node.js调用代码

const requestHandler = async(ctx) =>{
   const result = await sequelize.query('plpgsql function as described above');
   if(result)  {ctx.body=result; ctx.status = 200}
   else {ctx.status=409}
}

问题根源与修复方案

1. 核心错误原因

从错误栈的SQL内容可以看出,你的Node.js代码在并发执行函数定义语句create or replace function,而非仅调用函数。函数作为数据库对象,修改操作属于写操作,并发执行时必然触发元组更新冲突,这才是报错的根本原因。此外,函数中的commit语句完全无效:函数是纯查询逻辑,无DML操作,且return语句会终止函数执行,commit永远不会被执行。

2. 具体修复步骤

  • 修正Node.js调用逻辑:先在数据库中执行一次函数定义,之后每次请求仅调用函数:
const requestHandler = async(ctx) =>{
   const result = await sequelize.query('select * from getUserData() as fruit');
   if(result)  {ctx.body=result; ctx.status = 200}
   else {ctx.status=409}
}
  • 移除函数内无效代码:删掉函数末尾的commit;语句。
  • 优化函数查询逻辑:将两次查询合并为一次,减少IO开销与锁竞争:
create or replace function getUserData() returns json as 
$body$
begin
    return coalesce(
        (select 
            jsonb_build_object('user_id', u.id)
            from users as u 
            inner join users_to_groups utg on utg.user_id=u.id 
            inner join "groups" g on g.id = utg.group_id
            inner join users_to_permissions utp on utp.user_group_id = utg.id 
            inner join permissions p on p.id =utp.permission_id 
            where u.email= 'email@email.com' 
            and g.group_name = 'another Group' 
            and p."name" = 'some permission'
            and g.status_id = 1
            and exists(
                SELECT 1
                FROM users_to_groups utg2
                inner join "groups" g2 on g2.id = utg2.group_id 
                where utg2.user_id = u.id
                and g2.group_name ='necessary group
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 00:37:32