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 304 天前 via Android
悲观锁定能不用就不用,看上去你不需要锁定啊,你只是需要确保记录更新的事务性而已,那为什么不调高事物隔离级别?此外看了下您这个触发器,似乎默认的 RC 级别已经够用了
|