一、从分表到分库
从业务逻辑上,可以进行垂直分表,比如按记录的状态分表,不同状态进不同表,可以省略状态字段。
从数据规模上可以进行水平分表,按外键取模分表。
垂直分表可以从本质上降低业务复杂度,但对于数据规模问题,仍然需要分库解决。但要从根本上解决数据规模问题,需要基于水平分表再分库。
分表分库是一门艺术,需要针对实际业务具体分析。大的原则是要综合考虑关联查询成本、跨库事务一致性等。
跨库关联查询可通过以下方法解决:
- ①可在分库中冗余公共表;
- ②可通过业务代码进行两阶段查询。
跨库事务一致性可通过prisma提供的多schema接入和动态事务处理功能实现。
二、prisma的多schema接入方法
多schema接入有两种方法:一种是不同库共用同一个schema,这种情况常见于热备和负载均衡;另一种是不同库的schema也不同,这正适合于本文所讨论的分表分库话题。
(一)不同库共用一个schema
datasource db {
provider = "postgres"
url = env("PG_URL")
}
import { PrismaClient } from '@prisma/client'
const client1 = new PrismaClient({ datasources: { db: { url: 'postgres://localhost/db1' }} })
const client2 = new PrismaClient({ datasources: { db: { url: 'postgres://localhost/db2' }} })
从上面代码可看出,prisma客户端提供了对数据库url的覆写功能。
(二)不同库的schema不同
prisma/schema1.prisma:
datasource db {
provider = "postgres"
url = env("DB1_URL")
}
generator client {
provider = "prisma-client-js"
output = "./generated/client1"
}
model Model1 {
id Int @id @default(autoincrement())
model1Name String
}
prisma/schema2.prisma:
datasource db {
provider = "postgres"
url = env("DB2_URL")
}
generator client {
provider = "prisma-client-js"
output = "./generated/client2"
}
model Model2 {
id Int @id @default(autoincrement())
model2Name String
}
生成prisma客户端时,通过–schema参数指定具体的schema定义文件:
prisma generate --schema prisma/schema1.prisma
prisma generate --schema prisma/schema2.prisma
在业务代码中,可以如下使用多个prisma客户端:
import { PrismaClient as PrismaClient1 } from '../prisma/generated/client1'
import { PrismaClient as PrismaClient2 } from '../prisma/generated/client2'
const client1 = new PrismaClient1()
const client2 = new PrismaClient2()
三、prisma的交互式事务处理功能
prisma提供三种类型的事务操作:一种是在creatMany、include嵌套操作中原生支持事务性;一种是通过$transaction接口手工指定顺序执行的事务集合;一种是通过$transaction接口手工指定交互执行的事务异步函数。
(一)顺序执行
const [posts, totalPosts] = await prisma.$transaction([
prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
prisma.post.count(),
])
也可以在 $transaction 内使用原始查询:
const [userList, updateUser] = await prisma.$transaction([
prisma.$queryRaw`SELECT 'title' FROM User`,
prisma.$executeRaw`UPDATE User SET name = 'Hello' WHERE id = 2;`,
])
(二)交互执行
要使用交互式事务,可以将异步函数传递到$transaction。传递到该异步函数的第一个参数是Prisma Client的实例。 下面,将该实例称为tx。 在此tx实例上调用的任何数据库操作都会封装到事务中。
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
function transfer(from: string, to: string, amount: number) {
return prisma.$transaction(async (tx) => {
// 1. Decrement amount from the sender.
const sender = await tx.account.update({
data: {
balance: {
decrement: amount,
},
},
where: {
email: from,
},
})
// 2. Verify that the sender's balance didn't go below zero.
if (sender.balance < 0) {
throw new Error(`${from} doesn't have enough to send ${amount}`)
}
// 3. Increment the recipient's balance by amount
const recipient = await tx.account.update({
data: {
balance: {
increment: amount,
},
},
where: {
email: to,
},
})
return recipient
})
}
async function main() {
// This transfer is successful
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
// This transfer fails because Alice doesn't have enough funds in her account
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
}
main()
在上面的示例中,两个 update 查询都在数据库事务中运行。 当应用到达函数末尾时,事务是 committed 到数据库。
如果你的应用在此过程中遇到错误,异步函数将抛出异常并自动对事务进行 rollback。
要捕获异常,可以将 $transaction 封装在 try-catch 块中:
try {
await prisma.$transaction(async (tx) => {
// Code running in a transaction...
})
} catch (err) {
// Handle the rollback...
}
谨慎使用交互式事务。 长时间保持事务打开会损害数据库性能,甚至可能导致死锁。 尽量避免在事务函数内执行网络请求和执行缓慢的查询。
四、利用多schema接入和交互式事务实现跨数据库事务一致性
利用第二节中的多schema接入功能生成连接多个数据库的prisma客户端,在需要跨多数据库进行事务性操作时,可在第一个客户端启用交互式事务,在异步函数中启用第二个客户端的交互式事务,以此类推,从而实现整体的事务性。