Introduction
In PostgreSQL, a lock is a mechanism that prevents multiple database users from updating the same row or table simultaneously. Therefore, locks are useful in multi-user environments to secure shared resources during transactions to avoid breaking the application's logic. For instance, in an order processing application with thousands of users, you should design your software to lock a stock table to avoid overselling products. Similarly, in a ticketing application, locking the tickets table during the ticket grabbing process prevents overbooking.
To ensure the integrity and the maximum possible concurrent access to your data, the PostgreSQL server supports different lock modes. The most crucial database lock for Enterprise Resource Planning (ERP) applications is the access exclusive lock. This guide uses a managed PostgreSQL database cluster's access exclusive lock to implement an order processing application with Go on Ubuntu 20.04.
Prerequisites
To complete this guide:
Locate the PostgreSQL database cluster's Connection Details under the Overview tab. This guide uses the following sample connection details:
username:
rcsadminpassword:
EXAMPLE_POSTGRESQL_PASSWORDhost:
SAMPLE_POSTGRESQL_DB_HOST_STRING.rcsdb.comport:
16751
1. Set Up a Sample PostgreSQL Database
In this sample application, you require a sample database and the following tables:
customers: This table stores customers' data using thecustomer_id,first_name, andlast_namefields.products: This table stores products' data using theproduct_id,product_name,retail_price, andremaining_stockfields.customer_orders. This table tracks customers' orders using theorder_id,customer_id,product_id, andquantityfields.
When customers request an order, your sample application should lock the products table to check the remaining_stock field. If a customer orders a quantity that's greater than the remaining stock, the PostgreSQL application should cancel the transaction. Otherwise if the quantity is within the limit, the application should lock the products table, update the remaining_stock field and commit the transaction.
Follow the steps below to initialize the database and set up the sample tables:
Update your server's package information index.
$ sudo apt updateInstall the
postgresql-clientpackage. This lightweight command-line package lets you connect to your managed PostgreSQL cluster from your Linux server.$ sudo apt install -y postgresql-clientLog in to the managed PostgreSQL cluster. Replace
SAMPLE_POSTGRESQL_DB_HOST_STRING.rcsdb.comwith the correct host.$ psql -h SAMPLE_POSTGRESQL_DB_HOST_STRING.rcsdb.com -p 16751 -U rcsadmin defaultdbOutput.
Password for user rcsadmin:Enter the managed PostgreSQL database cluster password and press ENTER to proceed.
Output.
defaultdb=>Set up a sample
company_dbdatabase.defaultdb=> CREATE DATABASE company_db;Output.
CREATE DATABASEConnect to the new
company_dbdatabase.defaultdb=> \c company_db;Output.
... You are now connected to database "company_db" as user "rcsadmin".Create the
customerstable. TheSERIALkeyword instructs the PostgreSQL server to automatically generate uniquecustomer_idswhen you insert new records into the table.company_db=> CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50) );Output.
CREATE TABLEInsert sample data into the
customerstable.company_db=> INSERT INTO customers (first_name, last_name) VALUES ('JOHN', 'DOE'); INSERT INTO customers (first_name, last_name) VALUES ('MARY', 'SMITH'); INSERT INTO customers (first_name, last_name) VALUES ('PETER', 'JONES');Output.
... INSERT 0 1Query the
customerstable to ensure the data is in place.company_db=> SELECT customer_id, first_name, last_name FROM customers;Output.
customer_id | first_name | last_name -------------+------------+----------- 1 | JOHN | DOE 2 | MARY | SMITH 3 | PETER | JONES (3 rows)Create a
productstable. This table uses theremaining_stockfield to monitor the remaining stock for different products.company_db=> CREATE TABLE products ( product_id SERIAL PRIMARY KEY, product_name VARCHAR(50), retail_price DECIMAL(17, 2), remaining_stock INTEGER );Output.
CREATE TABLEInsert sample data into the
productstable.company_db=> INSERT INTO products (product_name, retail_price, remaining_stock) VALUES ('4G ROUTER', 55.23, 100); INSERT INTO products (product_name, retail_price, remaining_stock) VALUES ('SMARTWATCH', 75.25, 50); INSERT INTO products (product_name, retail_price, remaining_stock) VALUES ('PLIERS', 4.85, 20);Output.
... INSERT 0 1Query the
productstable to verify the data.company_db=> SELECT product_id, product_name, retail_price, remaining_stock FROM products;Output.
product_id | product_name | retail_price | remaining_stock ------------+--------------+--------------+----------------- 1 | 4G ROUTER | 55.23 | 100 2 | SMARTWATCH | 75.25 | 50 3 | PLIERS | 4.85 | 20 (3 rows)Create a
customer_orderstable. Instead of repeating the customers' names, this table uses thecustomer_idcolumn that links back to thecustomerstable to track customers' orders.company_db=> CREATE TABLE customer_orders ( order_id SERIAL PRIMARY KEY, customer_id INTEGER, product_id INTEGER, quantity INTEGER );Output.
CREATE TABLELog out from the managed PostgreSQL cluster.
company_db=> \q
The database schema is now ready. Follow the next step to create the main file that runs when you start your Go application.
2. Create a Go Application's Entry Point
Every Go application requires a main() function that executes when the application starts. Follow the steps below to create the function.
Make a new
projectdirectory for the application.$ mkdir projectNavigate to the new
projectdirectory.$ cd projectOpen a new
main.gofile on a text editor.$ nano main.goEnter the following information into the
main.gofile.package main import ( "net/http" "encoding/json" "fmt" ) func main() { http.HandleFunc("/orders", httpHandler) http.ListenAndServe(":8080", nil) } func httpHandler(w http.ResponseWriter, req *http.Request) { jsonPayload := map[string]interface{}{} err := json.NewDecoder(req.Body).Decode(&jsonPayload) if err != nil { fmt.Fprintf(w, "JSON error.") } else { response, err := saveData(jsonPayload) if err != nil { fmt.Fprintf(w, err.Error() + "\r\n") } else { enc := json.NewEncoder(w) enc.SetIndent("", " ") if err := enc.Encode(response); err != nil { fmt.Println(err.Error()) } } } }Save and close the
main.gofile.
The main.go file explained:
The
import(...)section loads packages required by the application. Thenet/httpmodule provides HTTP functionalities to the application. Theencoding/jsonpackage allows the application to work with the JSON data format. Thefmtpackage allows you to format strings and generate output.import ( "net/http" "encoding/json" "fmt" ) ...The
main(){...}functions runs when your application starts. Under the function, you're establishing a web server that listens for incoming HTTP requests on port8080. Then, you're delegating the requests to ahttpHandlerfunction.func main() { http.HandleFunc("/orders", httpHandler) http.ListenAndServe(":8080", nil) }The
httpHandler(...)function uses thejsonPayload := map[string]interface{}{}anderr := json.NewDecoder(req.Body).Decode(&jsonPayload)statements to parse the JSON payloads from HTTP clients. If an HTTP client sends a valid JSON, the application passes the requests to asaveData(jsonPayload)function using theresponse, err := saveData(jsonPayload)statement. This guide later shows you how to code thesaveData(...)function in a new file.func httpHandler(w http.ResponseWriter, req *http.Request) { ... }
After creating the main.go file, follow the next step to create a function for interacting with the managed PostgreSQL database cluster.
3. Create a Database Gateway File
You should always separate your Go application into multiple manageable files to assist troubleshooting when a problem occurs. This step shows you how to create a database file that connects to the managed PostgreSQL cluster to update several database tables to fulfill customers' orders.
Follow the steps below to create the file:
Open a new
database.gofile on a text editor.$ nano database.goEnter the following information into the
database.gofile. Replace thedbHost, anddbPassvalues with the correct managed PostgreSQL cluster's host and password.package main import ( "database/sql" "fmt" "errors" _ "github.com/lib/pq" ) func saveData(jsonPayload map[string]interface{}) (string, error){ customerId := jsonPayload["customer_id"] productId := jsonPayload["product_id"] orderQty := jsonPayload["quantity"].(float64) dbHost := "SAMPLE_POSTGRESQL_DB_HOST_STRING.rcsdb.com" dbPort := 16751 dbUser := "rcsadmin" dbPass := "EXAMPLE_POSTGRESQL_PASSWORD" dbName := "company_db" conString := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=require", dbHost, dbPort, dbUser, dbPass, dbName) db, err := sql.Open("postgres", conString) if err != nil { return "", err } defer db.Close() tx, err := db.Begin() if err != nil { return "", err } queryString := "lock table products in access exclusive mode" _, err = tx.Exec(queryString) if err != nil { tx.Rollback() return "", err } var remainingStock float64 queryString = "select remaining_stock from products where product_id = $1" row := tx.QueryRow(queryString, productId) err = row.Scan(&remainingStock) if err != nil { return "", err } if (remainingStock < orderQty) { tx.Rollback() return "", errors.New("The stock you are requesting is unavailable.") } queryString = "insert into customer_orders (customer_id, product_id, quantity) values ($1, $2, $3)" _, err = tx.Exec(queryString, customerId, productId, orderQty) if err != nil { tx.Rollback() return "", err } queryString = "update products set remaining_stock = remaining_stock - " + fmt.Sprintf("%f", orderQty) + " where product_id = $1" _, err = tx.Exec(queryString, productId) if err != nil { tx.Rollback() return "", err } err = tx.Commit() if err != nil { return "", err } return "Success", nil }Save and close the
database.gofile.
The database.go file explained:
The
import(...)section loads several packages required by your application. Thedatabase/sqlpackage provides a lightweight interface to the PostgreSQL database cluster. Thefmtpackage allows you to format strings. You're using theerrorspackage to craft and return custom errors. Thegithub.com/lib/pqpackage is a PostgreSQL database driver for Go.import ( "database/sql" "fmt" "errors" _ "github.com/lib/pq" ) ...Under the
saveData(...)function, you're loading the JSON payload from HTTP clients into thecustomerId,productId, andorderQtyvariables.func saveData(jsonPayload map[string]interface{}) (string, error){ customerId := jsonPayload["customer_id"] productId := jsonPayload["product_id"] orderQty := jsonPayload["quantity"].(float64) ...The following lines establish a connection to the managed PostgreSQL database cluster.
... dbHost := "SAMPLE_POSTGRESQL_DB_HOST_STRING.rcsdb.com" dbPort := 16751 dbUser := "rcsadmin" dbPass := "EXAMPLE_POSTGRESQL_PASSWORD" dbName := "company_db" conString := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=require", dbHost, dbPort, dbUser, dbPass, dbName) db, err := sql.Open("postgres", conString) if err != nil { return "", err } defer db.Close()In a PostgreSQL database, you must place the
LOCK TABLEstatement inside a transaction. You're accomplishing this using thetx, err := db.Begin()command. Then, you're passing thelock table products in access exclusive modecommand to lock theproductstable. TheLOCK TABLEstatement ensures only one database session acquires access to theproductstable when creating an order... tx, err := db.Begin() if err != nil { return "", err } queryString := "lock table products in access exclusive mode" _, err = tx.Exec(queryString) if err != nil { tx.Rollback() return "", err }After obtaining a
productstable lock, you're using theselect remaining_stock from products where product_id = $1query to check the remaining stock. Then, to ensure you're not overselling the product, you're using theif (remainingStock < orderQty) {}statement to verify the stock available before completing an order.... var remainingStock float64 queryString = "select remaining_stock from products where product_id = $1" row := tx.QueryRow(queryString, productId) err = row.Scan(&remainingStock) if err != nil { return "", err } if (remainingStock < orderQty) { tx.Rollback() return "", errors.New("The stock you are requesting is unavailable.") } ...After confirming the stock is available from the
productstable, you're inserting a new order in thecustomer_orderstable using theinsert into customer_orders (customer_id, product_id, quantity) values ($1, $2, $3)command. Then, you're updating theproductstable to decrement the stock depending on the customer's ordered quantity using theupdate products set remaining_stock = remaining_stock - " + fmt.Sprintf("%f", orderQty) + " where product_id = $1statement.queryString = "insert into customer_orders (customer_id, product_id, quantity) values ($1, $2, $3)" _, err = tx.Exec(queryString, customerId, productId, orderQty) if err != nil { tx.Rollback() return "", err } queryString = "update products set remaining_stock = remaining_stock - " + fmt.Sprintf("%f", orderQty) + " where product_id = $1" _, err = tx.Exec(queryString, productId) if err != nil { tx.Rollback() return "", err } ...The
tx.Commit()is the last command you're issuing to the PostgreSQL database cluster to commit the order to the database. After thetx.Commit()command, the PostgreSQL server releases all locks from theproductstable. This allows other clients to connect to the database and repeat the same procedure to make an order. ThesaveData(...)function, then returns aSuccessmessage to the callingmain()function that you created inside themain.gofile.err = tx.Commit() if err != nil { return "", err } return "Success", nil }
With the Go source code files ready, you can now test the application in the next step to ensure everything is working as expected.
4. Test the Sample Application
The final step is downloading the PostgreSQL driver for Go, running the application, and sending multiple Linux curl commands to test the application. Follow the steps below:
Download the PostgreSQL driver for Go from GitHub.
$ go get github.com/lib/pqStart the Go application.
$ go run ./Do not enter any other command in your active
SSHwindow because the previous command has a blocking function.Establish another
SSHconnection to your server and execute the following Linuxcurlcommands to send three sample order requests to the Go application.$ curl -i -X POST localhost:8080/orders -H "Content-Type: application/json" -d '{"customer_id": 1, "product_id": 1, "quantity": 3}' $ curl -i -X POST localhost:8080/orders -H "Content-Type: application/json" -d '{"customer_id": 2, "product_id": 2, "quantity": 5}' $ curl -i -X POST localhost:8080/orders -H "Content-Type: application/json" -d '{"customer_id": 3, "product_id": 3, "quantity": 15}'Verify the output below after running each
curlcommand. TheSuccessmessage shows that your Go application is working as expected.... "Success"Log in to the managed PostgreSQL database cluster to verify the data. Replace
SAMPLE_POSTGRESQL_DB_HOST_STRING.rcsdb.comwith the correct host.$ psql -h SAMPLE_POSTGRESQL_DB_HOST_STRING.rcsdb.com -p 16751 -U rcsadmin defaultdbEnter the password for the PostgreSQL database cluster and press ENTER to proceed.
Password for user rcsadmin:Connect to the
company_dbdatabase.defaultdb=> \c company_db;Output.
... You are now connected to database "company_db" as user "rcsadmin".Query the
productstable to ensure the stock is up to date depending on the orders you've placed using the previous Linuxcurlcommands.defaultdb=> SELECT product_id, product_name, retail_price, remaining_stock FROM products;Output.
product_id | product_name | retail_price | remaining_stock ------------+--------------+--------------+----------------- 1 | 4G ROUTER | 55.23 | 97 2 | SMARTWATCH | 75.25 | 45 3 | PLIERS | 4.85 | 5 (3 rows)Query the
customer_ordersandcustomerstables using aJOINstatement to ensure the orders are in place.defaultdb=> SELECT order_id, customer_orders.customer_id, customers.first_name, customers.last_name, quantity FROM customer_orders LEFT JOIN customers ON customer_orders.customer_id = customers.customer_id;Output.
order_id | customer_id | first_name | last_name | quantity ----------+-------------+------------+-----------+---------- 1 | 1 | JOHN | DOE | 3 2 | 2 | MARY | SMITH | 5 3 | 3 | PETER | JONES | 15 (3 rows)
The above query results confirm that your application's logic is working as expected.
Conclusion
This guide shows you how to use the PostgreSQL access exclusive lock to implement an order processing application with Go. With the table lock mechanism, the application supports concurrent orders without overselling the products. Use the same logic when designing mission-critical applications to ensure data consistency.
Check out the following guides to test more projects with rcs's managed database clusters: