CREATE TABLE IF NOT EXISTS account ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, username TEXT NOT NULL, coin NUMERIC NOT NULL DEFAULT 0 CHECK ( coin>= (0)::numeric ), version BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE ); comment on table account is '账户表.'; comment on column account.id is '自增唯一 ID 标示.'; comment on column account.username is '账户名.'; comment on column account.coin is '余额.'; comment on column account.version is '账户余额版本标识(乐观锁).'; CREATE TABLE IF NOT EXISTS transaction ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, account_id INTEGER NOT NULL REFERENCES account (id), value NUMERIC NOT NULL, balance NUMERIC NOT NULL DEFAULT 0 CHECK ( balance <> 'NaN'::numeric AND (balance >= (0)::numeric)), type INTEGER NOT NULL CHECK ( type IN (1, 2, 4, 8, 16) ), narration TEXT NOT NULL DEFAULT '', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE ); comment on table transaction is '账户余额变动交易表, 此表记录了每笔交易账户余额变动日志.'; comment on column transaction.id is '自增唯一 ID 标示.'; comment on column transaction.account_id is '账户 ID, 关联账户表.'; comment on column transaction.value is '交易代币数额,收入为正, 支出为负.'; comment on column transaction.balance is '交易完成后剩余的账户余额.'; comment on column transaction.narration is '交易描述.'; comment on column transaction.type is '交易类型.'; -- TRIGGER CREATE OR REPLACE FUNCTION transaction__sync_balance() RETURNS TRIGGER AS $$ BEGIN -- UPDATE UPDATE account SET coin = coin + NEW.value,version = version + 1 WHERE id = NEW.user_id RETURNING coin INTO NEW.balance; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER sync_balance_trigger BEFORE INSERT ON transaction FOR EACH ROW EXECUTE FUNCTION transaction__sync_balance();
SELECT pg_advisory_xact_lock(1001, account_id)
1 neoblackcap 2023-11-23 15:29:18 +08:00 我们以前的做法是尽量让锁前置,不用数据库。比如特定的竞争操作只有 master 进程有写入的权限。这样就去掉了锁竞争 |
2 frank000 2023-11-23 15:34:00 +08:00 这是必须要使用数据库触发器来做这件事么?还是有什么特别的考虑因素? |
4 ieesk OP @neoblackcap 这样改,我这业务牺牲有多大 |
5 binbin0915jjpp 2023-11-23 16:06:36 +08:00 放到 AP 端吧 比如 mybatis 的拦截器里 |
![]() | 6 MoYi123 2023-11-23 18:35:16 +08:00 为什么会有脏写? 是用 set coin = xxx 的写法吗? 为什么不用 set coin = coin + xxx? |
7 ZZ74 2023-11-23 19:03:35 +08:00 你用锁也没比其他的高性能啊,只是把压力放到了数据库层而已。性能就和数据库服务器性能强相关。 +钱操作简单 直接插入+更新即可。 -钱就是 where coin - xxx >0 更新成功就插入。或者代码层面分布式锁。 你要是想改的少,用存储过程或者 function 啊,也比触发器合适多了 |
8 neoblackcap 2023-11-23 21:10:23 +08:00 @ieesk 其实完全可以很少改动,你把写入的操作放在一个独立的服务,那个服务只有一个进程,开放一个接口。现有的服务在写入的时候就调用这个接口。 上锁,释放锁的速度并不慢。慢是因为锁竞争。单线程写入的话,性能上限应该可以逼近你数据的写入效率极限。 不过这样改的话,运维会多了很多工作。毕竟无缘故就多了一个服务需要运维,还让系统引入了一个单点问题。如果要解决单点问题的话,又要引入分布式锁。 |
9 iseki 2024-01-24 08:41:43 +08:00 via Android 悲观锁定能不用就不用,看上去你不需要锁定啊,你只是需要确保记录更新的事务性而已,那为什么不调高事物隔离级别?此外看了下您这个触发器,似乎默认的 RC 级别已经够用了 |