PostgreSQL AB表切换最佳实践 - 提高切换成功率,杜绝雪崩

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介:

标签

PostgreSQL , 雪崩 , 锁等待 , 切换表名 , DDL


背景

AB表切换经常出现在数据导入的场景中,例如每天或者每个固定周期,需要全量导入一批数据到数据库,同时被导入的数据要持续的被查询。

为了尽量避免导入影响查询,通常会使用AB表切换的方法。使用如下步骤:

1、建新表

2、全量数据导入到新表

3、在新表上面建必要的索引

4、切换新、老表名

但是,注意,由于切换表名是DDL操作,会加排它锁,与所有其他所会发送冲突。如果数据库中有长事务持有了老的表的任何锁,那么DDL会被堵塞,等待,同时会堵塞后来的任何需要持有老表锁的请求。

1、长事务,持有老表锁(共享锁)

2、切换表名,DDL,请求排他锁,等待,排它锁加入锁等待队列

3、其他会话,查询老表(请求共享锁),与锁等待队列中的DDL排他锁冲突,等待,共享锁加入锁等待队列。

以上情况,很容易造成雪崩。

《PostgreSQL 设置单条SQL的执行超时 - 防雪崩》

那么如何避免雪崩?并且在较短的时间内完成AB表切换呢?

1、杀持有新、旧表锁的会话  
  
2、在事务中切换表名  
  
开启事务  
  
设事务级锁超时  
  
对A,B表加排它锁  
  
切换A,B表  
  
重试若干次  
  
结束事务  

我们可以把以上步骤函数化,提供调用

切换表1和表2的函数接口如下(暴力版)

create or replace function exchange_tab(  
  nsp name,       -- schema name  
  from_tab name,  -- 表名1  
  to_tab name,    -- 表名2  
  mid_tab name,   -- 中间表名(使用不存在的表)  
  timeout_s int,  -- 锁超时时间(秒),建议设小一点,比如1秒  
  retry int,      -- 重试几次  
  steps int default 1    -- 重试次数判断  
) returns boolean as $$  
declare  
begin  
  -- 检查中间表是否已存在,存在则报错  
  perform 1 from pg_class where relname=mid_tab and relnamespace=(select oid from pg_namespace where nspname=nsp) limit 1;  
  if found then  
    raise notice 'you should use not exists table for exchange.';  
    return false;  
  end if;  
    
  -- 如果重试次数达到,还没有切换成功,则返回切换不成功  
  if steps >= retry then  
    return false;  
  end if;  
    
  -- 设置锁超时  
  execute format('set local lock_timeout=%L;', timeout_s||'s');  
    
  -- 杀死持有 表1,表2 锁的会话  
  -- 如果是普通用户,只能杀死同名用户下的其他会话,所以如果持锁的是其他用户,则需要使用超级用户才能杀死  
  perform pg_terminate_backend(pid) from pg_stat_activity where   
    pid in (select pid from pg_locks where   
              database=(select oid from pg_database where datname=current_database())   
              and relation in ((nsp||'.'||from_tab)::regclass, (nsp||'.'||to_tab)::regclass)  
           )   
    and pid<>pg_backend_pid();  
    
  -- 对表1,表2 加排他锁  
  execute format('lock table %I.%I in ACCESS EXCLUSIVE mode;', nsp, from_tab);  
  execute format('lock table %I.%I in ACCESS EXCLUSIVE mode;', nsp, to_tab);  
    
  -- 切换表1,表2  
  execute format('alter table %I.%I rename to %I;', nsp, to_tab, mid_tab);  
  execute format('alter table %I.%I rename to %I;', nsp, from_tab, to_tab);  
  execute format('alter table %I.%I rename to %I;', nsp, mid_tab, from_tab);  
    
  -- 返回切换成功  
  return true;  
  
  -- 任何一步失败(比如锁超时异常),则重试  
  exception when others then  
    -- 重试次数显示  
    raise notice 'retry: %', steps;  
      
    -- 如果重试次数达到,还没有切换成功,则返回切换不成功  
    if steps >= retry then  
      return false;  
    else  
      -- 递归调用,重试,传入参数重试次数+1.    
      return exchange_tab(nsp, from_tab, to_tab, mid_tab, timeout_s, retry, steps+1);  
    end if;  
end;  
$$ language plpgsql strict;  

例子

1、创建两张表,用来切换

create table abc(id int);  
create table abc_tmp(id int);  

2、分别写入100,1000条记录

insert into abc select generate_series(1,100);  
insert into abc_tmp select generate_series(1,1000);  

3、开启一个事务,查询ABC表,持有共享锁

begin;  
select * from abc limit 1;  
  id    
------  
    1  

4、在另一个会话中切换表abc, abc_tmp。瞬间切换成功

select * from exchange_tab(  
  'public',  
  'abc',  
  'abc_tmp',  
  'abc_notexists',  
  1,  
  10  
);  
  
  
 exchange_tab   
--------------  
 t  
(1 row)  

5、查询abc表的会话已被杀

postgres=# select * from abc;  
FATAL:  terminating connection due to administrator command  
server closed the connection unexpectedly  
        This probably means the server terminated abnormally  
        before or while processing the request.  
The connection to the server was lost. Attempting reset: Succeeded.  

6、检查是否切换成功

postgres=# select * from exchange_tab(  
  'public',  
  'abc',  
  'abc_tmp',  
  'abc_notexists',  
  1,  
  10  
);  
 exchange_tab   
--------------  
 t  
(1 row)  
  
postgres=# select count(*) from abc;  
 count   
-------  
  1000  
(1 row)  
  
postgres=# select count(*) from abc_tmp;  
 count   
-------  
   100  
(1 row)  
  
postgres=# select * from exchange_tab(  
  'public',  
  'abc',  
  'abc_tmp',  
  'abc_notexists',  
  1,  
  10  
);  
 exchange_tab   
--------------  
 t  
(1 row)  
  
postgres=# select count(*) from abc;  
 count   
-------  
   100  
(1 row)  
  
postgres=# select count(*) from abc_tmp;  
 count   
-------  
  1000  
(1 row)  

切换表1和表2的函数接口如下(可控版)

1、增加一个参数,是否杀持锁会话(暴力)。

2、增加一个SLEEP参数,在重试前,等待若干秒。

create or replace function exchange_tab( 
  nsp name,       -- schema name  
  from_tab name,  -- 表名1  
  to_tab name,    -- 表名2  
  mid_tab name,   -- 中间表名(使用不存在的表)  
  timeout_s int,  -- 锁超时时间(秒),建议设小一点,比如1秒  
  retry int,      -- 重试几次  
  kill boolean default false,  -- 是否执行terminate backend
  sleepts int default 1,  -- 重试前睡眠多少秒
  steps int default 1    -- 重试次数判断  
) returns boolean as $$  
declare  
begin  
  -- 检查中间表是否已存在,存在则报错  
  perform 1 from pg_class where relname=mid_tab and relnamespace=(select oid from pg_namespace where nspname=nsp) limit 1;  
  if found then  
    raise notice 'you should use not exists table for exchange.';  
    return false;  
  end if;  
    
  -- 如果重试次数达到,还没有切换成功,则返回切换不成功  
  if steps >= retry then  
    return false;  
  end if;  
    
  -- 设置锁超时  
  execute format('set local lock_timeout=%L;', timeout_s||'s');  
  
  if kill then
    -- 杀死持有 表1,表2 锁的会话  
    -- 如果是普通用户,只能杀死同名用户下的其他会话,所以如果持锁的是其他用户,则需要使用超级用户才能杀死  
    perform pg_terminate_backend(pid) from pg_stat_activity where   
      pid in (select pid from pg_locks where   
                database=(select oid from pg_database where datname=current_database())   
                and relation in ((nsp||'.'||from_tab)::regclass, (nsp||'.'||to_tab)::regclass)  
             )   
      and pid<>pg_backend_pid();  
  end if;
  
  -- 对表1,表2 加排他锁  
  execute format('lock table %I.%I in ACCESS EXCLUSIVE mode;', nsp, from_tab);  
  execute format('lock table %I.%I in ACCESS EXCLUSIVE mode;', nsp, to_tab);  
    
  -- 切换表1,表2  
  execute format('alter table %I.%I rename to %I;', nsp, to_tab, mid_tab);  
  execute format('alter table %I.%I rename to %I;', nsp, from_tab, to_tab);  
  execute format('alter table %I.%I rename to %I;', nsp, mid_tab, from_tab);  
    
  -- 返回切换成功  
  return true;  
  
  -- 任何一步失败(比如锁超时异常),则重试  
  exception when others then  
    -- 重试次数显示  
    raise notice 'retry: %', steps;  
      
    -- 睡眠
    perform pg_sleep(sleepts);
    
    -- 如果重试次数达到,还没有切换成功,则返回切换不成功  
    if steps >= retry then  
      return false;  
    else  
      -- 递归调用,重试,传入参数重试次数+1.    
      return exchange_tab(nsp, from_tab, to_tab, mid_tab, timeout_s, retry, kill, sleepts, steps+1);  
    end if;  
end;  
$$ language plpgsql strict;  

ab表切换时,如果其他SQL被锁等待堵塞,堵塞释放后到底操作的是a表还是b表?

插入操作

1、建表

create table abc(id int);  
  
create table abc_tmp(id int);  

2、插入测试,返回当前插入的是哪个表

postgres=# insert into abc values(1) returning *,tableoid,tableoid::regclass;  
 id | tableoid | tableoid   
----+----------+----------  
  1 |    26746 | abc  
(1 row)  
  
INSERT 0 1  

3、开启切换流程,先锁住2个要被切换的表

postgres=# begin;  
BEGIN  
postgres=# lock table abc in access exclusive mode ;  
LOCK TABLE  
postgres=# lock table abc_tmp in access exclusive mode ;  
LOCK TABLE  

4、开启另一个会话,插入,并返回当前插入的是哪个表?

postgres=# insert into abc values(1) returning *,tableoid,tableoid::regclass;  

堵塞,等待

5、切换流程继续,切换AB表

postgres=# alter table abc rename to abc_mid;  
ALTER TABLE  
postgres=# alter table abc_tmp rename to abc;  
ALTER TABLE  
postgres=# alter table abc_mid rename to abc_tmp;  
ALTER TABLE  
postgres=# end;  
COMMIT  

6、堵塞结束,到底插入了哪个表?

 id | tableoid | tableoid   
----+----------+----------  
  1 |    26743 | abc  
(1 row)  
  
INSERT 0 1  

结论:插入了切换后的ABC表

更新操作

接上面的表

truncate abc;  
truncate abc_tmp;  
  
insert into abc values (1);  
insert into abc_tmp values (1);  

1、更新测试,返回当前插入的是哪个表

update abc set id=2 returning *,ctid,tableoid,tableoid::regclass;  
  
 id | ctid  | tableoid | tableoid   
----+-------+----------+----------  
  2 | (0,2) |    26743 | abc  
(1 row)  
  
UPDATE 1  

2、开启切换流程,先锁住2个要被切换的表

postgres=# begin;  
BEGIN  
postgres=# lock table abc in access exclusive mode ;  
LOCK TABLE  
postgres=# lock table abc_tmp in access exclusive mode ;  
LOCK TABLE  

3、开启另一个会话,插入,并返回当前插入的是哪个表?

postgres=# update abc set id=2 returning *,ctid,tableoid,tableoid::regclass;  

堵塞,等待

4、切换流程继续,切换AB表

postgres=# alter table abc rename to abc_mid;  
ALTER TABLE  
postgres=# alter table abc_tmp rename to abc;  
ALTER TABLE  
postgres=# alter table abc_mid rename to abc_tmp;  
ALTER TABLE  
postgres=# end;  
COMMIT  

5、堵塞结束,到底更新了哪个表?

 id | ctid  | tableoid | tableoid   
----+-------+----------+----------  
  2 | (0,2) |    26746 | abc  
(1 row)  
  
UPDATE 1  

结论:更新了切换后的ABC表

维系同样索引、约束、默认值的AB表切换

业务上通常会使用中间表,加载全量数据后,再与业务表切换。除了数据,还需要考虑索引、约束、默认值等与业务表保持一致。

例子

1、交换表UDF(包括索引)

create or replace function exchange_table(v_nsp name, v_old_tbl name, v_new_tbl name) returns void as $$  
declare  
  idx_def text[];  
  sql text;  
begin  
  set lock_timeout ='5s';  
  select array_agg(regexp_replace(indexdef,'INDEX (.+) ON (.+) ','INDEX i'||to_char(clock_timestamp(),'yyyymmddhh24miss')||'_'||rn||' ON '||quote_ident(v_nsp)||'.'||quote_ident(v_new_tbl))) 
    into idx_def 
    from (select *,row_number() over() rn from pg_indexes where schemaname=v_nsp and tablename=v_old_tbl) t;  

  foreach sql in array idx_def   
  loop  
    execute sql;     
  end loop;  
  -- 如果索引非常多,可以异步并行创建,所有索引创建完成后再切换表  
  -- PG 11版本,不需要异步创建,因为单个索引的创建已经支持并行  
  execute format('drop table %I.%I', v_nsp, v_old_tbl);  
  execute format('alter table %I.%I rename to %I', v_nsp, v_new_tbl, v_old_tbl);  
end;  
$$ language plpgsql strict;  

PG11 并行创建索引例子

《PostgreSQL 快速给指定表每个字段创建索引》

异步并行调用参考

《阿里云RDS PostgreSQL OSS 外部表实践 - (dblink异步调用封装并行) 从OSS并行导入数据》

《在PostgreSQL中跑后台长任务的方法 - 使用dblink异步接口》

以上UDF,结合《PostgreSQL dblink异步调用实践,跑并行多任务 - 例如开N个并行后台任务创建索引, 开N个后台任务跑若干SQL》 改成并行创建

create or replace function exchange_table(v_nsp name, v_old_tbl name, v_new_tbl name) returns void as $$  
declare  
  idx_def text[];  
  sql text;  
  sqls text[];      
begin  
  
  set lock_timeout ='5s';  
  select array_agg(regexp_replace(indexdef,'INDEX (.+) ON (.+) ','INDEX i'||to_char(clock_timestamp(),'yyyymmddhh24miss')||'_'||rn||' ON '||quote_ident(v_nsp)||'.'||quote_ident(v_new_tbl))) 
    into idx_def 
    from (select *,row_number() over() rn from pg_indexes where schemaname=v_nsp and tablename=v_old_tbl) t;  
  foreach sql in array idx_def   
  loop  
    sqls := array_append(sqls, sql);  
  end loop;  
  -- 如果索引非常多,可以异步并行创建,所有索引创建完成后再切换表  
  -- PG 11版本,不需要异步创建,因为单个索引的创建已经支持并行  

  -- 并行
  perform run_sqls_parallel(  
    16,   -- 并行度  
    sqls  -- 执行index SQL数组  
  );  

  -- 切换表名
  execute format('drop table %I.%I', v_nsp, v_old_tbl);  
  execute format('alter table %I.%I rename to %I', v_nsp, v_new_tbl, v_old_tbl);  
end;  
$$ language plpgsql strict;  

2、DEMO

业务表\索引

create table tbl(id int primary key, info text not null, crt_time timestamp default now());  
create index idx_tbl_1 on tbl (crt_time);  
insert into tbl select generate_series(1,100),md5(random()::text);  

临时表(无索引,只包含与业务表一样的约束)

create table tmp (like tbl including CONSTRAINTS including DEFAULTS);   

加载数据到临时表(demo)

insert into tmp select generate_series(1,200),md5(random()::text);  

临时表切换为业务表,并加与业务表相同的索引

select exchange_table('public', 'tbl', 'tmp');  
  
postgres=# select count(*) from tbl;  
 count   
-------  
   200  
(1 row)  
  
postgres=# select * from tmp;  
ERROR:  relation "tmp" does not exist  
LINE 1: select * from tmp;  
                      ^  
postgres=# \d tbl  
                           Table "public.tbl"  
  Column  |            Type             | Collation | Nullable | Default   
----------+-----------------------------+-----------+----------+---------  
 id       | integer                     |           | not null |   
 info     | text                        |           | not null |   
 crt_time | timestamp without time zone |           |          | now()  
Indexes:  
    "i_20180827222503_1" UNIQUE, btree (id)  
    "i_20180827222503_2" btree (crt_time)  

小结

使用本文提到的方法对AB表进行切换,可以完全杜绝雪崩。同时,如果有正在使用AB表的会话,会被回退,由于切换AB表仅涉及元数据的更新,所以切换很快,影响很小。

注意,使用切换表的方法,实际上有一个问题是依赖关系的问题,如果依赖关系复杂,RENMAE的方式,需要注意依赖关系的切换,包括主外键,继承,分区等。

所以,对于依赖关系复杂的情况,更好的方法是类似pg_repack的方法,切换表的filenode(即数据文件映射关系,当然还需要注意TOAST也要切换),而不是RENAME TABLE。

参考

《PostgreSQL 设置单条SQL的执行超时 - 防雪崩》

《PostgreSQL 锁等待监控 珍藏级SQL - 谁堵塞了谁》

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
16天前
|
SQL 关系型数据库 PostgreSQL
把PostgreSQL的表导入SQLite
把PostgreSQL的表导入SQLite
15 0
|
3月前
|
SQL canal 算法
PolarDB-X最佳实践:如何设计一张订单表
本文主要内容是如何使用全局索引与CO_HASH分区算法(CO_HASH),实现高效的多维度查询。
|
5月前
|
SQL 关系型数据库 数据库
postgresql中连接两张表更新第三张表(updata)
如何结合两张表的数据来更新第三张表
66 0
|
3月前
|
关系型数据库 分布式数据库 数据处理
报名预约|PolarDB产品易用性创新与最佳实践在线直播
在线体验PolarDB产品易用性创新,练习阿里云数据库NL2SQL、无感切换实操技能,探索数据处理提速与学习成本降低实践
|
4月前
|
关系型数据库 数据库 PostgreSQL
postgresql | 数据库| 生成2000W条的简单测试表
postgresql | 数据库| 生成2000W条的简单测试表
24 0
|
4月前
|
关系型数据库 MySQL 分布式数据库
PolarDB-X最佳实践系列(三):如何实现高效的分页查询
分页查询是数据库中常见的操作。本文将介绍,如何在数据库中(无论是单机还是分布式)高效的进行翻页操作。
112399 10
|
6月前
|
安全 关系型数据库 数据库
《确保安全:PostgreSQL安全配置与最佳实践》
《确保安全:PostgreSQL安全配置与最佳实践》
225 0
|
6月前
|
存储 关系型数据库 Go
《PostgreSQL备份与恢复:步骤与最佳实践》
《PostgreSQL备份与恢复:步骤与最佳实践》
339 0
|
7月前
|
关系型数据库 MySQL 分布式数据库
PolarDB MySQL从表回收站中恢复误删的表
PolarDB MySQL提供表回收站的功能,删除的表会被临时转移到表回收站,本案例教您从表回收站恢复误删的表。
220 0
|
8月前
|
Cloud Native 关系型数据库 分布式数据库
客户说|PolarDB最佳实践:工期缩短2/3,揭秘极氪APP分布式改造效率神器
极氪APP引入阿里云PolarDB,21天完成数据库分布式改造

相关产品

  • 云原生数据库 PolarDB