前言
最近在学习Serverless架构相关的知识,学习过程中发现一个有趣的现象:无论是教程示例,还是场景实例,Serverless架构中鲜有出现数据库的身影。各类文章所介绍的Serverless架构应用场景中,也几乎都是无需数据库的业务场景。在一些教程文章中,对于一些需要进行数据存储的场景,通常的做法是将数据存储在 JSON 文件中,然后上传到对象存储服务中,在搜索相关资料的过程中甚至还发现了SQLite 对象存储这种很硬核的数据存储方式,这些方法显然只能应对简单的数据存储。那么数据库作为互联网时代的基石,从单体架构到微服务架构,其都扮演着举足轻重的角色,为何偏偏在Serverless架构中存在感这么低呢?
从用户的角度来看,Serverless架构虽然有着免运维、弹性伸缩、按需付费等优点,但同时由于其本身构建复杂,扩展性不强,维护困难等缺点,用户一般只会用其来实现一些简单的业务,追求的是低成本、轻量级、无需维护长期运行的服务器。此时如果引入传统的数据库,整个架构就会重新变得厚重,用户看中的优势也被完全破坏了,违背了使用Serverless架构的初衷。
那么,有什么方案可以解决这个问题呢?答案就是让数据库也Serverless化,让数据库也具备免运维、弹性伸缩、按需付费等特点,这就是近两年比较火热的Serverless数据库。刚好在CSDN看到腾讯云 TDSQL-C Serverless 产品测评活动,可以免费体验腾讯云推出的Serverless数据库产品TDSQL-C Serverless。本篇博文就带大家一起,使用腾讯云云函数 TDSQL-C Serverless实现一个“时光邮局”,体验全栈Serverless的魅力。
一、TDSQL-C Serverless简介
TDSQL 是腾讯云自研的新一代云原生关系型数据库。融合了传统数据库、云计算与新硬件技术的优势,100% 兼容 MySQL,为用户提供极致弹性、高性能、高可用、高可靠、安全的数据库服务。实现超百万 QPS 的高吞吐、PB 级海量分布式智能存储、Serverless 秒级伸缩,助力企业加速完成数字化转型。
TDSQL-C Serverless 服务是腾讯云自研的新一代云原生关系型数据库 TDSQL-C MySQL 版的无服务器架构版,是全 Serverless 架构的云原生数据库。TDSQL-C Serverless 服务支持按实际计算和存储资源使用量收取费用,不用不付费,将腾讯云云原生技术普惠用户。其架构特点如下:
- 按需启动,不需要时可关闭。
- 自动扩展/收缩。
- 缩放对应用程序无影响。
TDSQL-C Serverless 服务优势:
自动驾驶(Autopilot):数据库根据业务负载自动启动停止,无感扩缩容,扩缩容过程不会断开连接。
按使用计费(Utility Pricing):按实际使用的计算和存储量计费,不用不付费,按秒计量,按小时结算。
二、云函数 TDSQL-C Serverless实现“时光邮局”
1.购买TDSQL-C Serverless实例
TDSQL-C Serverless购买地址:https://buy.cloud.tencent.com/cynosdb,关键配置说明:
- 实例形态选择Serverless
- 网络选择:后续创建云函数时,需要选择与这里一致的VPC及子网
- 算力配置:弹性伸缩的关键配置,与购买传统云数据库需要挑选固定规格不同的是,TDSQL-C Serverless只需要配置最小CCU和最大CCU即可。CCU(TDSQL-C Compute Unit)为 Serverless 的计算计费单位,1CCU约等于1C2G的计算资源。根据配置的CCU范围,TDSQL-C Serverless可以在这个区间内实现自动的弹性伸缩
- 自动暂停:按需付费的关键配置,数据库在设定时间内无连接将自动进入暂停状态,暂停后计算将不再计费。当有连接访问时,系统会秒级自动启动处于暂停状态的数据库,用户不需设置重连机制。
- 其他购买配置与传统数据库大同小异,根据自身需求配置即可
2.建库建表
通过DMC数据库管理工具可以快速的完成建库建表等操作,建表语句:
代码语言:javascript复制CREATE TABLE `future_email` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`date` date DEFAULT NULL,
`email` varchar(50) DEFAULT '',
`letter` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4
3.创建云函数
- 关键配置:启用私有网络,保证可以内网访问TDSQL-C Serverless
- 创建API网关触发器、定时触发器
- 云函数代码
'''
原作者:乂乂又又
原文链接:https://cloud.tencent.com/developer/article/1618588
修改说明:原文采用JSON文件 COS实现数据存储,本文修改为使用TDSQL-C Serverless作为数据存储方案
'''
# -*- coding: utf-8 -*-
import json
import datetime
import random
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
import smtplib
import pymysql
# 配置TDSQL-C Serverless连接信息
host = '172.16.0.3'
port = 3306
user = 'root'
password = 'xxxxxxx'
database = 'test'
#配置发件邮箱
mail_host = "smtp.163.com"
mail_user = "xxxx@163.com"
mail_pass = "xxxxxxxxxxxx"
mail_port = 465
#smtp邮箱实例
smtpObj = smtplib.SMTP_SSL(mail_host, mail_port)
#获取所有信件
def getletters():
db = pymysql.connect(host=host,port=port,user=user,password=password,database=database)
cursor = db.cursor()
# SQL 查询语句
sql = "SELECT * FROM future_email WHERE date = %s"
try:
cursor.execute(sql, (today()))
results = cursor.fetchall()
data_list = []
for row in results:
data_list.append(list(row))
return data_list
except:
print("Error: unable to fetch data")
db.close()
#添加信件
def addletter(date, email, letter):
db = pymysql.connect(host=host, port=port, user=user, password=password, database=database)
cursor = db.cursor()
# SQL 插入语句
sql = "INSERT INTO future_email (date, email, letter) VALUES (%s, %s, %s)"
try:
cursor.execute(sql, (date, email, letter))
db.commit()
print("Data inserted successfully.")
except pymysql.Error as e:
print(f"MySQL Error {e.args[0]}: {e.args[1]}")
db.rollback()
return False
db.close()
return True
#删除信件
def delletter(id):
db = pymysql.connect(host=host, port=port, user=user, password=password, database=database)
cursor = db.cursor()
# SQL 删除语句
sql = "DELETE FROM future_email WHERE id = %s"
try:
cursor.execute(sql, (id))
db.commit()
print("Data deleted successfully.")
except pymysql.Error as e:
print(f"MySQL Error {e.args[0]}: {e.args[1]}")
db.rollback()
db.close()
# 获取今日日期
def today():
return datetime.datetime.now().strftime("%Y-%m-%d")
# 根据时间生成uuid
def randomKey():
return ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba0123456789', 6))
# api网关回复消息格式化
def apiReply(reply, html=False, code=200):
htmlStr = r'''<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>给未来的自己写封信</title>
<style>
html,
body {
padding: 0px;
margin: 0px;
height: 100vh;
}
.main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main_phone {
display: flex;
flex-direction: column;
justify-content: start;
align-items: center;
}
</style>
</head>
<body id='body'>
<div class="main" style="width: 80vw;">
<div style="height: 5vh;"></div>
<div id='letter_top'>
<p style="text-align: center;">开始写信</p>
<wired-textarea id="letter" style="height: 320px;width: 300px;" placeholder="此刻平静地写下一封信,给未来的自己一份温暖..." elevation="6" rows="14"></wired-textarea>
</div>
<div style="display: flex;align-items: center;justify-content: center;">
<div id='letter_left'>
<p style="text-align: center;">开始写信</p>
<wired-textarea id="letter" style="height: 320px;width: 300px;" placeholder="此刻平静地写下一封信,给未来的自己一份温暖..." elevation="6" rows="14"></wired-textarea>
</div>
<div style="width: 16px;"></div>
<div>
<p style="text-align: center;">送信日期</p>
<wired-calendar id="calendar"></wired-calendar>
</div>
</div>
<wired-divider style="margin: 16px 0;"></wired-divider>
<p id="hitokoto"></p>
<div>
<wired-input id="email" placeholder="收件邮箱"></wired-input>
<wired-button onclick="send()">投递</wired-button>
</div>
<div style="height: 5vh;"></div>
</div>
<script>
let datex = '';
let myEmail = document.getElementById('email');
let myLetter = document.getElementById('letter');
let myCalendar = document.getElementById('calendar');
let width =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
let height =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight
let pc = width >= height
let today = new Date();
let info = today.toString().split(' ');
let selected = `${info[1]} ${today.getDate()}, ${today.getFullYear()}`;
document.getElementById('body').classList.add(pc ? 'main' : 'main_phone');
if(pc){
document.getElementById('letter_top').remove();
document.getElementById('letter_left').style.display = 'block';
myLetter = document.getElementById('letter');
} else {
document.getElementById('letter_top').style.display = 'block';
document.getElementById('letter_left').remove();
myLetter = document.getElementById('letter');
}
myCalendar.setAttribute("selected", selected);
myCalendar.addEventListener('selected', () => {
let selectedObject = myCalendar.value;
let date = new Date(new Date().setDate(selectedObject.date.getDate()));
datex = date.toISOString().substr(0, 10);
});
function send() {
console.log(datex, myEmail.value, myLetter.value)
if (datex.length < 1 || myEmail.value.length < 1 || myLetter.value.length < 1) {
alert('信件内容、送信日期或投递邮箱不能为空');
return;
}
fetch(window.location.href, {
method: 'POST',
body: JSON.stringify({
date: datex,
email: myEmail.value,
letter: myLetter.value
})
}).then(res => res.json())
.catch(error => console.error('Error:', error))
.then(response => alert(response.ok ? '添加成功:)' : '添加失败:('));
}
</script>
<script src="https://v1.hitokoto.cn/?encode=js&select=#hitokoto" defer></script>
<script src="https://unpkg.com/wired-elements@2.0.5/lib/wired-elements-bundled.js"></script>
</body>
</html>'''
return {
"isBase64Encoded": False,
"statusCode": code,
"headers": {'Content-Type': 'text/html' if html else 'application/json', "Access-Control-Allow-Origin": "*"},
"body": htmlStr if html else json.dumps(reply, ensure_ascii=False)
}
#登陆邮箱
def loginEmail():
try:
smtpObj.login(mail_user, mail_pass)
return True
except smtplib.SMTPException as e:
print(e)
return False
#发送邮件
def sendEmail(letter):
message = MIMEText(letter[3], 'plain', 'utf-8')
message['From'] = formataddr(('时间邮局', mail_user))
message['To'] = letter[2]
message['Subject'] = '一封来自很久以前的信'
try:
smtpObj.sendmail(mail_user, letter[2], message.as_string())
print("send email success")
return True
except smtplib.SMTPException as e:
print(f"Send EMail Error {e.args[0]}: {e.args[1]}")
return False
#每天定时检查需要发送的信件
def check_send_letters():
loginEmail()
letters = getletters()
for letter in letters :
if letter[1] == datetime.date.today():
status = sendEmail(letter)
if(status):
delletter(letter[0])
def main_handler(event, context):
if 'Time' in event.keys(): # 来自定时触发器
check_send_letters()
return
if 'httpMethod' in event.keys(): # 来自api网关触发器
if event['httpMethod'] == 'GET':
return apiReply('', html=True) # 返回网页
if event['httpMethod'] == 'POST': # 添加信件
body = json.loads(event['body'])
flag = addletter(body['date'], body['email'], body['letter'])
return apiReply({
'ok': True if flag else False,
'message': '添加成功' if flag else '添加失败'
})
return apiReply('', html=True)
4.查看效果
- 页面效果
- TDSQL-C Serverless内存储的数据
- 邮件效果
5.TDSQL-C Serverless状态以及账单
- 当发生请求时的资源使用情况,可以清晰的看到TDSQL-C Serverless的自动启动过程
- 根据购买时配置的自动暂停时间,10分钟后TDSQL-C Serverless已自动暂停
- 账单
总结
TDSQL-C Serverless是一款完全符合Serverless特征的关系型数据库产品,无需运维,弹性伸缩,按需付费。有了它,数据库将不再是Serverless架构的“短板”,Serverless架构的落地场景也将不再局限于简单业务的处理,当遇到数据存储需求时,也不用再退而求其次的去使用JSON文件 对象存储的方案。
单从架构优势上来说,TDSQL-C Serverless的出现,打破了Serverless架构落地的最后一关,极大的丰富了Serverless架构的应用落地场景,用户可以体验到从前端、到后端、再到数据存储落地的全栈Serverless。
TDSQL-C Serverless继承了Serverless架构的优点的同时,不可避免的也会存在Serverless的一些缺点,最直观的一个缺点就是冷启动时间过长,云函数当前已经可以做到毫秒级的冷启动,但TDSQL-C Serverless的冷启动时长却还在秒级。希望TDSQL-C Serverless在后续的版本可以持续优化这个耗时,将腾讯云云原生技术普惠用户。