React Email与PostgreSQL:关系型邮件事务处理方案
·
React Email与PostgreSQL:关系型邮件事务处理方案
痛点:现代邮件系统的数据一致性挑战
在当今的数字化时代,邮件系统已成为企业通信的核心基础设施。然而,传统的邮件发送方案往往面临以下痛点:
- 数据不一致:邮件发送状态与业务数据分离,导致状态同步困难
- 事务处理缺失:邮件发送失败时,业务操作无法回滚
- 性能瓶颈:高并发场景下,邮件队列处理效率低下
- 监控困难:缺乏完整的邮件发送生命周期追踪
解决方案架构设计
系统架构图
数据库表结构设计
-- 邮件模板表
CREATE TABLE email_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
subject VARCHAR(500) NOT NULL,
react_component TEXT NOT NULL,
variables JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 邮件发送队列表
CREATE TABLE email_queue (
id SERIAL PRIMARY KEY,
template_id INTEGER REFERENCES email_templates(id),
recipient_email VARCHAR(255) NOT NULL,
recipient_data JSONB DEFAULT '{}',
status VARCHAR(50) DEFAULT 'pending',
send_attempts INTEGER DEFAULT 0,
last_attempt_at TIMESTAMP,
scheduled_for TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sent_at TIMESTAMP,
error_message TEXT
);
-- 邮件发送日志表
CREATE TABLE email_logs (
id SERIAL PRIMARY KEY,
queue_id INTEGER REFERENCES email_queue(id),
provider VARCHAR(100),
message_id VARCHAR(255),
status VARCHAR(50),
response JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
核心实现方案
1. React Email组件集成
import { render } from '@react-email/render';
import { WelcomeEmail } from '../emails/welcome-email';
import { db } from '../lib/database';
export async function sendWelcomeEmail(userId: number, userEmail: string) {
const client = await db.connect();
try {
await client.query('BEGIN');
// 1. 获取用户数据
const userResult = await client.query(
'SELECT name, activation_token FROM users WHERE id = $1',
[userId]
);
if (userResult.rows.length === 0) {
throw new Error('用户不存在');
}
const user = userResult.rows[0];
// 2. 渲染React Email组件
const emailComponent = WelcomeEmail({
name: user.name,
activationLink: `https://example.com/activate/${user.activation_token}`
});
const htmlContent = await render(emailComponent);
const plainTextContent = await render(emailComponent, { plainText: true });
// 3. 插入邮件队列记录
const queueResult = await client.query(
`INSERT INTO email_queue
(template_id, recipient_email, recipient_data, status)
VALUES ($1, $2, $3, 'pending')
RETURNING id`,
[1, userEmail, JSON.stringify({ userId, userName: user.name })]
);
const queueId = queueResult.rows[0].id;
// 4. 发送邮件(事务内操作)
const emailService = getEmailService();
const result = await emailService.send({
to: userEmail,
subject: '欢迎加入我们!',
html: htmlContent,
text: plainTextContent
});
// 5. 更新邮件状态
await client.query(
`UPDATE email_queue
SET status = 'sent', sent_at = NOW(), message_id = $1
WHERE id = $2`,
[result.messageId, queueId]
);
// 6. 记录发送日志
await client.query(
`INSERT INTO email_logs
(queue_id, provider, message_id, status, response)
VALUES ($1, $2, $3, 'delivered', $4)`,
[queueId, 'resend', result.messageId, JSON.stringify(result)]
);
await client.query('COMMIT');
return { success: true, messageId: result.messageId };
} catch (error) {
await client.query('ROLLBACK');
// 记录失败状态
if (queueId) {
await client.query(
`UPDATE email_queue
SET status = 'failed', error_message = $1
WHERE id = $2`,
[error.message, queueId]
);
}
throw error;
} finally {
client.release();
}
}
2. 邮件模板管理系统
// email-templates/welcome-email.tsx
import {
Body,
Container,
Head,
Heading,
Html,
Preview,
Text,
Button,
Section,
} from '@react-email/components';
interface WelcomeEmailProps {
name: string;
activationLink: string;
}
export const WelcomeEmail = ({ name, activationLink }: WelcomeEmailProps) => (
<Html>
<Head />
<Preview>欢迎加入我们的平台!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>欢迎,{name}!</Heading>
<Text style={text}>
感谢您注册我们的服务。请点击下面的按钮激活您的账户。
</Text>
<Section style={btnContainer}>
<Button style={button} href={activationLink}>
激活账户
</Button>
</Section>
<Text style={text}>
如果您没有请求此邮件,请忽略它。
</Text>
</Container>
</Body>
</Html>
);
const main = {
backgroundColor: '#ffffff',
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
};
const container = {
margin: '0 auto',
padding: '20px 0 48px',
width: '580px',
};
const h1 = {
color: '#333',
fontSize: '24px',
fontWeight: 'bold',
margin: '30px 0',
padding: '0',
};
const text = {
color: '#333',
fontSize: '16px',
lineHeight: '24px',
margin: '16px 0',
};
const btnContainer = {
textAlign: 'center' as const,
margin: '32px 0',
};
const button = {
backgroundColor: '#0066ff',
borderRadius: '6px',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
textAlign: 'center' as const,
padding: '12px 24px',
};
3. 事务性邮件处理工作流
高级特性实现
1. 批量邮件发送优化
export async function sendBulkEmails(
templateId: number,
recipients: Array<{ email: string; data: any }>,
batchSize: number = 100
) {
const totalBatches = Math.ceil(recipients.length / batchSize);
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
const batch = recipients.slice(
batchIndex * batchSize,
(batchIndex + 1) * batchSize
);
await processEmailBatch(templateId, batch);
}
}
async function processEmailBatch(templateId: number, batch: Array<{ email: string; data: any }>) {
const client = await db.connect();
try {
await client.query('BEGIN');
const template = await getEmailTemplate(templateId);
const emailService = getEmailService();
for (const recipient of batch) {
const renderedEmail = await renderEmailTemplate(template, recipient.data);
const queueResult = await client.query(
`INSERT INTO email_queue (...) VALUES (...) RETURNING id`,
[templateId, recipient.email, JSON.stringify(recipient.data)]
);
const result = await emailService.send({
to: recipient.email,
subject: template.subject,
html: renderedEmail.html,
text: renderedEmail.text
});
await updateEmailStatus(client, queueResult.rows[0].id, result);
}
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
2. 邮件状态监控与重试机制
-- 重试失败邮件的函数
CREATE OR REPLACE FUNCTION retry_failed_emails(max_attempts INT DEFAULT 3)
RETURNS INT AS $$
DECLARE
retry_count INT := 0;
queue_record RECORD;
BEGIN
FOR queue_record IN
SELECT * FROM email_queue
WHERE status = 'failed'
AND send_attempts < max_attempts
AND last_attempt_at < NOW() - INTERVAL '5 minutes'
LOOP
BEGIN
PERFORM retry_single_email(queue_record.id);
retry_count := retry_count + 1;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE '重试邮件 % 失败: %', queue_record.id, SQLERRM;
END;
END LOOP;
RETURN retry_count;
END;
$$ LANGUAGE plpgsql;
性能优化策略
数据库索引优化
-- 为频繁查询的字段创建索引
CREATE INDEX idx_email_queue_status ON email_queue(status);
CREATE INDEX idx_email_queue_recipient ON email_queue(recipient_email);
CREATE INDEX idx_email_queue_scheduled ON email_queue(scheduled_for);
CREATE INDEX idx_email_logs_queue_id ON email_logs(queue_id);
CREATE INDEX idx_email_logs_created_at ON email_logs(created_at);
-- 部分索引优化
CREATE INDEX idx_pending_emails ON email_queue(id)
WHERE status = 'pending' AND scheduled_for <= NOW();
连接池与并发控制
import { Pool } from 'pg';
const dbPool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // 最大连接数
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// 使用连接池处理高并发
async function processHighVolumeEmails() {
const promises = emailList.map(async (email) => {
const client = await dbPool.connect();
try {
await processSingleEmail(client, email);
} finally {
client.release();
}
});
await Promise.all(promises);
}
监控与告警系统
邮件发送统计查询
-- 每日邮件发送统计
SELECT
DATE(created_at) as send_date,
status,
COUNT(*) as count,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (PARTITION BY DATE(created_at)) as percentage
FROM email_queue
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY DATE(created_at), status
ORDER BY send_date DESC, status;
-- 邮件提供商性能统计
SELECT
provider,
COUNT(*) as total_emails,
AVG(EXTRACT(EPOCH FROM (sent_at - created_at))) as avg_delivery_time_seconds,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as delivery_rate
FROM email_logs
GROUP BY provider
ORDER BY delivery_rate DESC;
部署与运维最佳实践
Docker容器化部署
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
EXPOSE 3000
CMD ["node", "dist/index.js"]
环境变量配置
# 数据库配置
DB_HOST=postgres
DB_PORT=5432
DB_NAME=email_service
DB_USER=email_user
DB_PASSWORD=secure_password
# 邮件服务配置
EMAIL_PROVIDER=resend
RESEND_API_KEY=your_resend_api_key
# 性能配置
MAX_CONCURRENT_EMAILS=50
EMAIL_BATCH_SIZE=100
RETRY_ATTEMPTS=3
总结与展望
React Email与PostgreSQL的结合为现代邮件系统提供了强大的事务处理能力。通过这种架构,我们实现了:
- 数据一致性:ACID事务保证邮件发送与业务操作的原子性
- 高可靠性:完善的错误处理和重试机制
- 可扩展性:支持高并发邮件发送场景
- 可观测性:完整的邮件发送生命周期追踪
未来可以进一步优化的方向包括:
- 引入Redis缓存提升模板渲染性能
- 实现基于WebSocket的实时邮件状态推送
- 集成机器学习算法优化邮件发送时机
- 增加A/B测试功能优化邮件模板效果
这种架构方案特别适合电商、SaaS、金融等对数据一致性要求极高的场景,为企业级邮件通信提供了可靠的技术保障。
更多推荐

所有评论(0)