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