LemonHX

LemonHX

CEO of Limit-LAB 喜欢鼓捣底层的代码,意图改变世界
twitter
tg_channel

Postgres 多租户尝试🌟

当我们谈论多租户时,可以想象成一栋公寓大楼。 🏢 在这栋大楼中,有许多不同的公寓🏠(租户),每个公寓都是独立的,有自己的空间和隐私,但它们共享同一个大楼的基础设施(例如电梯、水电等)。 在软件领域中,多租户指的是一种架构模式,允许在同一套软件系统中为多个不同的用户(租户)提供服务,每个用户之间相互隔离,就像公寓大楼中的住户一样。

  • 为什么 PostgreSQL 需要多租户? 🤔

PostgreSQL 需要支持多租户的能力,因为很多应用程序需要为多个用户提供服务,并且这些用户需要独立的数据存储空间。 🛠️ 通过多租户架构,PostgreSQL 可以实现将不同用户的数据隔离存储,确保数据安全性和隐私性。 🔒

  • 为什么不给每个用户都开个 PostgreSQL? 🤨

给每个用户都开设一个独立的 PostgreSQL 数据库将导致资源的浪费和管理的复杂性增加。 📈 通过多租户架构,可以更有效地利用数据库资源,降低成本并简化管理。 🔄 此外,多租户架构还可以提高系统的可伸缩性和灵活性。 💡

  • 为什么 SAAS 需要多租户? 🌐

在软件即服务(SAAS)模式下,服务提供商需要为多个客户提供统一的软件服务,每个客户可能是一个组织、企业或个人。 🤝 使用多租户架构可以实现不同客户之间数据的隔离和安全性,同时避免为每个客户维护单独的系统实例,降低运维复杂性和成本。 💻

预备知识 📚

手里有 docker,之前学过 PG,有一定的 SQL 基础,了解什么是触发器,什么是函数
触发器

建立测试环境#

docker run --name mt -e POSTGRES_PASSWORD=114514 -d postgres:16

docker exec -it mt bash

psql -U postgres 连接数据库

CREATE DATABASE testmt;

假设有表 User 和 Access#

CREATE TABLE Users (
    id SERIAL PRIMARY KEY, -- UUID INSTEAD THIS IS JUST FOR DEMO
    name VARCHAR(50) NOT NULL
);

CREATE TABLE Accesses (
    id SERIAL PRIMARY KEY, -- UUID INSTEAD THIS IS JUST FOR DEMO
    user_id INTEGER NOT NULL,
    access_name VARCHAR(50) NOT NULL,
    access_secret VARCHAR(50) NOT NULL,
    -- FOR CASCADE DELETE AND UPDATE
    FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE ON UPDATE CASCADE
);

新建 trigger#

  1. 获取当前 insert 的用户 id
  2. 在 access 表中插入一条记录
CREATE FUNCTION insert_default_access() RETURNS trigger AS $trigger_bound$
BEGIN
    INSERT INTO Accesses (user_id, access_name, access_secret)
    VALUES (NEW.id, NEW.id, 'random_secret');
    RETURN NEW;
END;
$trigger_bound$
LANGUAGE plpgsql;

CREATE TRIGGER create_default_access_for_new_user
AFTER INSERT ON Users
FOR EACH ROW
EXECUTE PROCEDURE insert_default_access();

测试

INSERT INTO Users (name) VALUES ('Alice');
SELECT * FROM Users;
--  id | name
-- ----+-------
--   2 | Alice
-- (1 row)
SELECT * FROM Accesses;
--  id | user_id | access_name | access_secret
-- ----+---------+-------------+---------------
--   2 |       2 | 2           | random_secret
-- (1 row)
DELETE FROM Users WHERE id = 2;

看起来没问题

开始创建多租户#

新建 trigger
当插入 Accesses 时,创建一个新的用户,并创建一个与之绑定的 schema

为每个 access 创建一个用户#

CREATE FUNCTION create_pg_account() RETURNS trigger AS $trigger_bound$
BEGIN
    EXECUTE FORMAT('CREATE USER user_%s_%s WITH PASSWORD %L', NEW.user_id, NEW.access_name, NEW.access_secret);
    RETURN NEW;
END;
$trigger_bound$
LANGUAGE plpgsql;

CREATE TRIGGER auto_create_pg_account
AFTER INSERT ON Accesses
FOR EACH ROW
EXECUTE PROCEDURE create_pg_account();

测试

INSERT INTO Users (name) VALUES ('Bob');
SELECT * FROM Users;
--  id | name
-- ----+------
--   3 | Bob
-- (1 row)
SELECT * FROM Accesses;
--  id | user_id | access_name | access_secret
-- ----+---------+-------------+---------------
--   3 |       3 | 3           | random_secret
-- (1 row)

使用\du查看用户

\du

--                              List of roles
--  Role name |                         Attributes
-- -----------+------------------------------------------------------------
--  postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
--  user_3_3  |

看起来没问题

DELETE FROM Users WHERE id = 3;

创建 schema,并授权#

CREATE FUNCTION create_schema() RETURNS trigger AS $trigger_bound$
BEGIN
    EXECUTE FORMAT('CREATE SCHEMA IF NOT EXISTS schema_%s', NEW.user_id);
    -- main user has all privileges
    -- sub users only have select, insert for main user's tables
    IF CAST(NEW.user_id AS VARCHAR(50)) = NEW.access_name THEN
        EXECUTE FORMAT('GRANT ALL PRIVILEGES ON SCHEMA schema_%s TO user_%s_%s', NEW.user_id, NEW.user_id, NEW.access_name);
    ELSE
        EXECUTE FORMAT('GRANT SELECT, INSERT ON SCHEMA schema_%s TO user_%s_%s', NEW.user_id, NEW.user_id, NEW.access_name);
    END IF;
    -- set search path
    EXECUTE FORMAT('ALTER USER user_%s_%s SET search_path TO schema_%s', NEW.user_id, NEW.access_name, NEW.user_id);
    RETURN NEW;
END;
$trigger_bound$
LANGUAGE plpgsql;

CREATE TRIGGER auto_create_schema_for_access
AFTER INSERT ON Accesses
FOR EACH ROW
EXECUTE PROCEDURE create_schema();

测试

INSERT INTO Users (name) VALUES ('Charlie');
SELECT * FROM Users;

使用\dn查看 schema

\dn
SELECT * FROM pg_user;
--  usename  | usesysid | usecreatedb | usesuper | userepl | usebypassrls |  passwd  | valuntil |       useconfig
-- ----------+----------+-------------+----------+---------+--------------+----------+----------+------------------------
--  postgres |       10 | t           | t        | t       | t            | ******** |          |
--  user_5_5 |    16439 | f           | f        | f       | f            | ******** |          | {search_path=schema_5}

可以自己登录一下试试,这里不再赘述

当 Accesses 更新时,同步更新用户密码#

CREATE FUNCTION update_pg_account() RETURNS trigger AS $trigger_bound$
BEGIN
    EXECUTE FORMAT('ALTER USER user_%s_%s WITH PASSWORD %L', NEW.user_id, NEW.access_name, NEW.access_secret);
    RETURN NEW;
END;
$trigger_bound$
LANGUAGE plpgsql;

CREATE TRIGGER auto_update_pg_account
AFTER UPDATE ON Accesses
FOR EACH ROW
EXECUTE PROCEDURE update_pg_account();

可以自己登录一下试试旧密码上不去就行

当 Accesses 删除记录时,同步删除用户#

CREATE FUNCTION delete_pg_account() RETURNS trigger AS $trigger_bound$
BEGIN
    -- revoke all privileges on schema
    EXECUTE FORMAT('REVOKE ALL PRIVILEGES ON SCHEMA schema_%s FROM user_%s_%s', OLD.user_id, OLD.user_id, OLD.access_name);
    EXECUTE FORMAT('DROP USER IF EXISTS user_%s_%s', OLD.user_id, OLD.access_name);
    RETURN OLD;
    -- delete schema if the user is dead
    IF CAST(OLD.user_id AS VARCHAR(50)) = OLD.access_name THEN
        EXECUTE FORMAT('DROP SCHEMA IF EXISTS schema_%s CASCADE', OLD.user_id);
    END IF;
END;
$trigger_bound$
LANGUAGE plpgsql;

CREATE TRIGGER auto_delete_pg_account
AFTER DELETE ON Accesses
FOR EACH ROW
EXECUTE PROCEDURE delete_pg_account();

测试

DELETE FROM Accesses WHERE id = 5;
\du
\dn

结语#

这只是一个简单的多租户实现,实际应用中可能会更复杂,比如需要更多的权限控制、数据隔离等。 🌟 但是,通过这个例子,你可以了解到如何使用 PostgreSQL 的触发器和函数来实现最基础的多租户功能,若想继续深究可以去看下面的功能:

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.