23/12/2019 - GO, MYSQL
Aşağıdaki liste bazı yararlı bilgiler içermektedir ve veritabanına dayalı Go uygulaması ile çalışırken çok faydalı olabilir, bu nedenle önerileri kendi kodunuzda test etmek ve uygulamak size kalmıştır. Bilgilerin çoğu Go database/sql tutorial ve The Ultimate Guide To Building Database - Driven Apps with Go dökümanlarından derlenmiştir.
Burada öğreneceğiniz ve dikkat etmeniz gereken en önemli şey db.Prepare fonksiyonunun kullanılmasıdır. Önerileri takip etmezseniz, fonksiyonunun gerçekten ne yaptığını ve nasıl yaptığını anlamazsanız, bu durum gerçekten sizin ve uygulamanız için biraz can sıkıcı olabilir! Aşağıda yazılan bilgilerin haricinde, yukarıda belirtilen ikinci linkteki dökümanı okumanızı şiddetle tavsiye ederim çünkü, konular hakkında daha ayrıntılı bilgiler bulabilirsiniz.
SQL sorgularınızın (insert, update, delete vs.) bir parçası olarak kullandığınız verilerin temiz olduğundan %100 eminseniz (SQL enjeksiyonsuz), db.Prepare yerine db.Exec kullanmayı tercih etmelisiniz. Bunun nedeni ise, sorgunuz içinde argümanlar için bir yer tutucusu olsada olmasada, tek bir sorguyu db.Prepare ile çalıştırmak isterseniz toplam üç ekstra sorgu (Prepare, Execute ve Close) kullanılacaktır ki, bu sistem kaynaklarını ziyan etmek anlamına gelir. Eğer db.Exec kullanırsanız toplam bir sorgu (Query) kullanılacaktır. Aşağıdaki örnek, tabloya tek bir kayıt eklemek içindir. Gördüğümüz gibi db.Prepare ağ üzerinde üç tur atarken, db.Exec sadece bir tur atıyor. Not: Eğer parametreleri kendiniz temizleyip kullanmak isterseniz fmt.Sprintf() fonksiyonunu %q yer tutucusu ile kullanabilirsiniz - örnek: fmt.Sprintf(`INSERT INTO table (name, age, active) VALUES (%q, %d, %t)`)
func Insert() (int64, error) {
stmt, err := db.Prepare("INSERT INTO users (name) VALUES (?)")
if err != nil {
return 0, err
}
defer stmt.Close()
res, err := stmt.Exec("inanzzz")
if err != nil {
return 0, err
}
id, err := res.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
2019-12-28T21:12:53.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz')
2019-12-28T21:12:53.527352Z 3 Close stmt
func Insert() (int64, error) {
res, err := db.Exec("INSERT INTO user (name) VALUES ('inanzzz')")
if err != nil {
return 0, err
}
id, err := res.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
2019-12-28T21:13:11.337554Z 3 Query INSERT INTO user (name) VALUES ('inanzzz')
100 kullanıcı, 10 saniye ara vermeden aynı anda istek gönderiyor. Aşağıdaki bulgular sadece kaba hesaplamalardır.
// db.Prepare
Average total requests: 4500
Average request duration: 210ms
// db.Exec
Average total requests: 8500
Average request duration: 120ms
Eğer aynı sorguyu birden fazla değişik parametre ile kullanacaksanız, stmt.Exec fonksiyonu döngünün içinde tutulurken, db.Prepare fonksiyonun mutlaka döngünün dışında tutulması gereklidir. Bunun nedeni ise, döngü içinde aynı sorgunun sürekli hazırlanıp ve ardından kapatılması sistem kaynaklarının ziyanından başka hiçbir anlam ifade etmez. Ayrıca, döngü içinde defer stmt.Close() kullanılması kaynak sızıntısına neden olacağı için, gerçekleşmesi muhtemel olan bir felaketi beklemeye benzer. Sonuç olarak, döngü içinde db.Prepare kullanımı, dışarıda kullanımına nazaran sorgu miktarını iki katına çıkarır. Aşağıdaki örneklere bakın.
func Insert() error {
for i := 1; i < 4; i++ {
stmt, err := r.database.Prepare(`INSERT INTO users (name) VALUES (?)`)
if err != nil {
return err
}
defer stmt.Close()
_, err := stmt.Exec(fmt.Sprintf("inanzzz-%d", i))
if err != nil {
return err
}
}
return nil
}
Gördüğümüz gibi toplam olarak 9 sorgu çalıştırıldı.
2019-12-28T21:12:53.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-1')
2019-12-28T21:12:54.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:54.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-2')
2019-12-28T21:12:55.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:55.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-3')
2019-12-28T21:12:56.527352Z 3 Close stmt
2019-12-28T21:12:56.527352Z 3 Close stmt
2019-12-28T21:12:56.527352Z 3 Close stmt
func Insert() error {
stmt, err := db.Prepare(`INSERT INTO users (name) VALUES (?)`)
if err != nil {
return err
}
defer stmt.Close()
for i := 1; i < 4; i++ {
_, err := stmt.Exec(fmt.Sprintf("inanzzz-%d", i))
if err != nil {
return err
}
}
return nil
}
Gördüğümüz gibi toplam olarak 5 sorgu çalıştırıldı.
2019-12-28T21:12:53.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-1')
2019-12-28T21:12:54.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-2')
2019-12-28T21:12:55.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-3')
2019-12-28T21:12:55.527352Z 3 Close stmt
result.LastInsertId() ve result.RowsAffected() fonksiyonlarını sadece son eklenen kaydın ne olduğunu veya kaç kaydın eklendiğini açıkça bilmeniz gerekiyorsa kullanın. Aksi takdirde bunları varsayılan olarak kullanmayın çünkü, bu işlemler veritabanına erişmeseler bile, ilişkide oldukları veritabanı bağlantıları o an için meşgulse, bu durum "bekletme" işlemi haline gelebilir. Ayrıca, tüm veritabanı sürücüleri de bu özellikleri desteklemez.
Eğer sorgunuz veri çekmek için değilse, db.Query ve db.QueryRow yerine db.Exec kullanmanız daha mantıklı olacaktır. Bunun ana nedeni, db.Exec bağlantısını havuza anında geri bırakırken, db.Query bağlantısını rows.Close() çağrılıncaya kadar havuzun dışında tutar. Eğer bunu yapmazsanız, bağlantı "sızıntısına" ve sunucunun kullanılabilir bağlantılarının tükenmesine neden olabilirsiniz.
Eğer select sorgusu kullanıyorsanız ve de sorgunuz içinde argümanlar için bir yer tutucusu olsada olmasada, db.Prepare & stmt.Exec kombinasyonun yerine, db.Query ve db.QueryRow kullanmaya devam etmelisiniz. Bunun nedeni ise, sorgunuz içinde argümanlar için bir yer tutucusu yok ise, db.Query ve db.QueryRow otomatik olarak db.Exec gibi çalışarak ve sadece bir sorgu (Query) kullanılacaktır ki bu da istediğimiz senaryodur.
Bu ayar serbest bırakıldıktan sonra havuzda boşta kalması gereken bağlantıların miktarını tanımlar. Veritabanında SHOW PROCESSLIST; sorgusunu kullanarak doğrulama yapabilirsiniz. Varsayılan değer 2'dir. Bu varsayılan değer, çok sayıda bağlantının birbirleri ardına kapatılmasına ve açılmasına neden olur ki, bu da kaçınmak istediğiniz bir şeydir. Ekstra çalışma ve gecikme süresinin yanı sıra, yeni bir bağlantı beklerken gecikmeye yol açar. Güvenli bir şekilde, bunun yerine 25 veya 5 (ideal) gibi daha yüksek bir değere ayarlayabilirsiniz.
100 kullanıcı aynı anda 10 saniye ara vermeden istek gönderiyor. Aşağıdaki bulgular sadece kaba hesaplamalardır.
// db.SetMaxIdleConns = 2 (default)
Average total requests: 3800
Average request duration: 250ms
// db.SetMaxIdleConns = 5
Average total requests: 4700
Average request duration: 200ms
// db.SetMaxIdleConns = 25
Average total requests: 5500
Average request duration: 180ms
Veritabanındaki mevcut açık bağlantı sayısını tanımlar. Varsayılan değer sınırsızdır. Varsayılan değerini değiştirmek istiyorsanız dikkatli olmalısınız. Herhangi bir sorgu içeriği context.WithTimeout gibi bir zaman aşımı özelliği uyguluyorsa, en ufak bir değişiklik potansiyel olarak context deadline exceeded hatalarına yol açabilir. Bekleyen sorguları işlemek için yeterli açık bağlantı olmayabilir, bu nedenle birçok sorgu zaman aşımına uğrar. Ayrıca performans düşecektir. %100 emin olmadığınız sürece bunu değiştirmekten kaçınma daha iyidir.
Bir bağlantının maksimum kullanılabilirlik süresini tanımlar. Varsayılan değer sınırsızdır. İdeal zaman ayrıca db.SetMaxIdleConns değerine de bağlıdır yani, db.SetMaxIdleConns yükselince db.SetConnMaxLifetime düşer. Kaba örnek: db.SetMaxIdleConns: 5 - db.SetConnMaxLifetime: 30 minutes, db.SetMaxIdleConns: 10 - db.SetConnMaxLifetime: 15 minutes. Çok yoğun bir veritabanına dayalı uygulamanız varsa, sayıları daha yüksek tutabilirsiniz. Uzun ömürlü birçok boş bağlantı kullanmak, yüksek sistem kaynakları gerektirir.
Bir döngü içinde defer rows.Close() satırlarını çağırmayın. Potansiyel olarak bellek veya bağlantı "sızıntısına" neden olacaktır.
Uygulama önyüklemesinde sql.DB örneğini yalnızca bir kez oluşturun ve tüm isteklerin bunu kullanmasına izin verin. Aksi takdirde uygulama veritabanına birçok TCP bağlantısını açıp kapatacak ve muhtemelen sunucuyu kilitleyecektir.
Ertelemeden rows.Close() fonksiyonunu çalıştırın, aksi takdirde bağlantı "sızıntısına" neden olabilirsiniz.
Elinizden geldiği sürece performansı artırmak için db.Prepare kullanmayarak daha az sorgu çalıştırabilir ve aynı sorgular için gereksiz ağ gidiş-dönüş gezilerini sonlandırabilirsiniz. fmt.Sprintf() fonksiyonunu %q yer tutucusu ile kullanarak sorgu parametrelerini kendiniz temizleyip kullanabirsiniz.
Eğer Select sorgusu kullanmıyorsanız db.Query ve db.QueryRow kullanmayın. Onun yerine db.Exec kullanın.
Bir tablodaki alan boş değer döndürüyorsa, struct veya değişken türünüz de boş değer olarak ayarlanmalıdır. Örnek: sql.NullString, mysql.NullTime vs.
Süprizlerden hoşlanmıyorsanız fmt.Sprint() kullanarak uint64 parametreleri öncelikle string verilerine çevirin çünkü, db.Query, db.QueryRow ve db.Exec fonksiyonları uint64 parametreleriyle zaman zaman sorun yaşayabiliyorlar