概念
多版本并发控制(MVCC)
- MVCC就是对同一份数据临时保留多版本的方式,实现并发控制。它可以避免读写事务之间的互相阻塞,相比通常的封锁技术可极大的提高业务的并发性能。
- 当一个MVCC 数据库需要更一个一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。
- 这种方式允许读者读取在他读之前已经存在的数据,即使这些在读的过程中半路被别人修改、删除了,也对先前正在读的用户没有影响。这种多版本的方式避免了填充删除操作在内存和磁盘存储结构造成的空洞的开销,但是需要系统周期性整理(sweepthrough)以真实删除老的、过时的数据。
表膨胀
- 根据MVCC的原理,数据库没有办法直接更新数据(更新操作(update)是通过先删除(delete)再插入(insert)实现的),被更新之前的行数据仍然在数据文件中。这种现象叫做表膨胀。
- PG中的MVCC
- 数据文件中存放同一逻辑行的多个行版本(称为Tuple);
- 每个行版本的头部记录创建该版本的事务ID以及删除该行版本的事务的ID(分别称为xmin和xmax);
- 每个事务的状态(运行中,中止或提交)记录在
pg_clog
文件中; - 根据上面的数据并运用一定的规则每个事务只会看到一个特定的行版本。
实验
查看当前隔离级别:
新建一个简单的表:
create table test(id int primary key, value char(10) );
insert into test values(1,'cc');
insert into test values(2,'dd');
insert into test values(3,'ee');
-- 开始一个事务,但不提交
begin;update test set value = 'ccc' where id = 1;
-- 再开一个事物,查看
begin;select *, xmin,xmax,cmin,cmax from test;
-- 最后再提交
commit;
在事务A中:
事务B中:
由于是分布式数据库,并不能根据事务的id来判断先后,可参考这里。
解决方法
vacuum full table
当一个过期的行不在被任何活跃事务引用时,它可以被移除从而腾出其所占用的空间进行重用。VACUUM命令会标记过期行所使用的空间可以被重用。
VACUUM命令(不带FULL)可以与其他查询并行运行。它会标记之前被过期行所占用的空间为空闲可用。如果剩余的空闲空间数量可观,它会把该页面加到该表的空闲空间映射中。当Greenplum数据库之后需要空间分配给新行时,它首先会参考该表的空闲空间映射以寻找有可用空间的页面。如果没有找到这样的页面,它会为该文件追加新的页面。
VACUUM(不带FULL)不会合并页面或者减小表在磁盘上的尺寸。它回收的空间只是放在空闲空间映射中表示可用。为了阻止磁盘文件大小增长,重要的是足够频繁地运行VACUUM。运行VACUUM的频率取决于表中更新和删除(插入只会增加新行)的频率。重度更新的表可能每天需要运行几次VACUUM来确保通过空闲空间映射能找到可用的空闲空间。在运行了一个更新或者删除大量行的事务之后运行VACUUM也非常重要。
VACUUM FULL命令会把表重写为没有过期行,并且将表减小到其最小尺寸。表中的每一页都会被检查,其中的可见行被移动到前面还没有完全填满的页面中。空页面会被丢弃。该表会被一直锁住直到VACUUM FULL完成。相对于常规的VACUUM命令来说,它是一种非常昂贵的操作,可以用定期的清理来避免或者推迟这种操作。最好是在一个维护期来运行VACUUM FULL。VACUUM FULL的一种替代方案是用一个CREATE TABLE AS语句重新创建该表并且删除掉旧表。
但是在索引存在的情况下,vacuum full table
只能删除之前行,不能回收索引。
因此使用vacuum full
回收垃圾的建议操作流程:
- 记录下表的索引
- 删除索引
- vacuum full 表
- 重建索引
-- 创建表
create table stu(id int, name varchar(20),gender varchar(10));
-- 生成数据
insert into stu select generate_series(1,100000),'Bob','man';
-- 查看表大小
select pg_size_pretty(pg_relation_size('stu'));
-- 修改数据
update stu set name='Alice';
-- 再次查看表大小
select pg_size_pretty(pg_relation_size('stu'));
-- 使用vacuum full stu回收空间,并查看空间大小。此时表空间应缩小一半。
vacuum full stu;
-- 查看表大小
select pg_size_pretty(pg_relation_size('stu'));
-- 在id字段上创建索引,并查看索引大小
create index idx_stu on stu(id);
-- 查看表大小
select pg_size_pretty(pg_relation_size('stu'));
-- 更新表数据,并查看表空间和索引空间大小
update stu set name = 'Jack';
select pg_size_pretty(pg_relation_size('idx_stu'));
select pg_size_pretty(pg_relation_size('stu'));
-- 删除索引,回收表空间。
drop index idx_stu;
vacuum full stu;
-- 重建索引,查看表空间和索引大小
create index idx_stu on stu(id);
select pg_size_pretty(pg_relation_size('stu'));
select pg_size_pretty(pg_relation_size('idx_stu'));
修改分布键释放空间
修改分布键可以回收索引的膨胀空间。修改分布键加载的锁是排它锁。建议在没有业务的时候执行,不要影响业务。
缺点:由于需要重新修改分布键,计算开销比较大,在表分布已经比较均匀时会影响性能。
-- 更新表stu的name字段,并查看表空间和索引大小
update stu set name = 'Jay';
select pg_size_pretty(pg_relation_size('idx_stu'));
select pg_size_pretty(pg_relation_size('stu'));
-- 查看表格分布
\d stu;
-- 按照原有的分布键重新分布。查看表空间和索引空间
alter table stu set with (reorganize=true) distributed by(id);
select pg_size_pretty(pg_relation_size('stu'));
select pg_size_pretty(pg_relation_size('idx_stu'));
复制表
创建新表,导入数据 CREATE TABLE…AS SELECT
命令把该表拷贝为一个新表,新建的表将不会出现膨胀现象。
-- 更新表,并创建复制表。
update stu set name = 'Bob';
create table copy_stu as
select * from stu;
-- 删除原表,并将复制表名更改为stu。
drop table stu;
alter table copy_stu rename to stu;