英文:
Using transactions in Go
问题
当用户注册时,我的应用程序会执行两个操作:将用户添加到数据库,并发送验证电子邮件:
...
err := collection("users").Insert(&u);
if err != nil {
WriteServerError(w, err)
return
}
if err = sendVerificationEmail(&u); err != nil {
WriteServerError(w, err)
}
...
如果电子邮件发送失败,我不希望将用户添加到数据库中;同样,如果用户未添加到数据库中,我也不希望发送电子邮件(后者当然可以通过按照这个代码块的顺序来解决)。
假设Go支持事务,那么对于这样的情况是否值得使用事务?如果是的话,有人可以给我一些指导,告诉我如何转换上面的代码吗?
我可以使用嵌套语句,但这可能会变得非常混乱。
英文:
When a user registers, my app does two things - it adds the user to the database and it sends a verification email:
...
err := collection("users").Insert(&u);
if err != nil {
WriteServerError(w, err)
return
}
if err = sendVerificationEmail(&u); err != nil {
WriteServerError(w, err)
}
...
I wouldn't want to add the user to the database if the email does not get sent nor would I want to send the email if the user didn't get added to the database (the latter is of course taken care of with the code blocks in this order).
Assuming Go supports transactions, is it worth bothering with for something like this? If so then could someone give me some pointers as to how I would convert this code above?
I could use nested statements but this could get very ugly.
答案1
得分: 1
在Go语言中没有"transaction"的概念。你可以按照正确的顺序执行以下步骤:
- 将用户插入数据库。
- 如果插入失败,返回。
- 发送电子邮件。
- 如果发送邮件失败,删除用户。
一般来说,通常不需要执行第4步。解决这个问题的常见方法是在注册/登录页面上添加一个"我没有收到验证邮件,请重新发送"的按钮。即使你正确发送了邮件,也无法确定它是否被反垃圾邮件过滤、误点击删除等。
英文:
There is no "transaction" in Go. The better you can do is to take the steps in the right order:
- Insert the user in the database
- If the insertion failed, return
- Send the email
- If the email failed, remove the user
That said, generally speaking, there is no need to go to step 4. The common approach to this problem is to have a "I didn't get the verification email, please send another." button somewhere along your registration/login pages. You can't even be sure that your email arrived even if you sent it correctly: it could be filtered by the anti-spam, deleted by a missclick, etc.
答案2
得分: 1
首先,你需要问自己为什么会出现数据库/电子邮件发送错误。如果你的数据库不可靠,插入操作有2%的失败率,那么你可能需要选择其他数据库。如果问题是数据库出现故障,那么你需要担心的问题不仅仅是几封未发送的电子邮件。
如果数据库可靠,问题是因为你使用了某个第三方提供商的API有时会失败导致无法发送电子邮件,那么你可以使用以下代码:
for {
if err = sendVerificationEmail(&u); err != nil {
WriteServerError(w, err)
} else {
break
}
}
另一种方法是将应该发送的电子邮件信息保存在数据库中,并使用另一个进程(每隔X秒运行一次)检查所有未发送的电子邮件并发送它们。一旦电子邮件发送成功,从表中删除该条目。
英文:
I think that first of all you have to ask yourself why can you get a database/email sending error. If your database is unreliable and 2% of the time the insert fails, then may be you have to chose some other database. If the problem is that database failed, then you have to worry about way more problems than just a couple of unsend emails.
If the database is reliable and the problem is with sending email because you use some 3rd party provider and sometimes the API fails, than you can use something like
for {
if err = sendVerificationEmail(&u); err != nil {
WriteServerError(w, err)
} else {
break
}
}
Another approach is to save information about the emails that should be saved in a database and use another process (which runs every X seconds) to check for all unsend emails and send them. Once the email is send, remove the entry from the table.
答案3
得分: 0
最好的做法是将用户以未验证状态添加到数据库中。如果他们在几周后仍未进行验证,可以将其清除。
英文:
It's best to just add the user to the database in an unverified state. You can clean them out a few weeks later if they never verify.
答案4
得分: 0
你可以使用数据库事务来实现这个功能。
- 开始事务。
- 插入新用户。
- 尝试发送电子邮件。
- 如果成功,提交事务。
- 否则回滚。
示例代码:
package main
import (
"database/sql"
_ "github.com/lib/pq"
"log"
"net/smtp"
)
func main() {
db, err := sql.Open("postgres", "user=user dbname=postgres")
if err != nil {
log.Fatal(err)
return
}
defer db.Close()
tnx, err := db.Begin()
if err != nil {
log.Fatal(err)
return
}
user := "test"
password := "testpassword"
res, err := tnx.Exec("INSERT INTO users(login, password) VALUES (?, ?)", user, value)
if err != nil {
log.Fatal(err)
err := tnx.Rollback()
if err != log.Fatal(err) {
log.Fatal(err)
}
return
}
err := smtp.SendMail(
"smpt.google.com", smtp.Auth{}, "test@from.com",
[]string{"test@to.com"}, []byte("Hello"))
if err != nil {
log.Fatal(err)
err := tnx.Rollback()
if err != log.Fatal(err) {
log.Fatal(err)
}
return
}
err := tnx.Commit()
if err != nil {
log.Fatal(err)
return
}
}
是的,当提交事务时,你可以捕获死锁或数据库连接丢失的情况,但这种情况非常罕见,你不必担心。或者,如果你想要100%的成功保证,可以使用临时用户创建方案。
英文:
You can use database transaction for this.
- Begin transaction.
- Insert new user.
- Try to send email.
- If ok, commit transaction.
- Else rollback
Sample code:
package main
import (
"database/sql"
_ "github.com/lib/pq"
"log"
"net/smtp"
)
func main() {
db, err := sql.Open("postgres", "user=user dbname=postgres")
if err != nil {
log.Fatal(err)
return
}
defer db.Close()
tnx, err := db.Begin()
if err != nil {
log.Fatal(err)
return
}
user := "test"
password := "testpassword"
res, err := tnx.Exec("INSERT INTO users(login, password) VALUES (?, ?)", user, value)
if err != nil {
log.Fatal(err)
err := tnx.Rollback()
if err != log.Fatal(err) {
log.Fatal(err)
}
return
}
err := smtp.SendMail(
"smpt.google.com", smtp.Auth{}, "test@from.com",
[]string{"test@to.com"}, []byte("Hello"))
if err != nil {
log.Fatal(err)
err := tnx.Rollback()
if err != log.Fatal(err) {
log.Fatal(err)
}
return
}
err := tnx.Commit()
if err != nil {
log.Fatal(err)
return
}
}
Yes, you can catch deadlock or database connection lost when commit transaction, but this is so rare case, you don't worry about it. Or if you want 100% success guarantee, use temporary user creation scheme.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论