SoFunction
Updated on 2025-03-05

golang is based on mysql and simply implements distributed read and write locks

Business scenarios

Because the project has just been launched, there is no plan to introduce other middleware at the moment, so I plan to implement distributed read and write locks through mysql; and this business scenario also meets the distributed read and write lock scenario. The abstract business scenario is: specific resource X, which can perform 2 operations: read operations and write operations.The following conditions are required for the two operations:

  • The machine that performs operations is distributed in different nodes, that is, distributed;
  • Read operations are shared, that is, multiple goroutines can be used to perform read operations on resource X at the same time;
  • Write operations are mutually exclusive, which means that only one goroutine is allowed to perform write operations on resource X at the same time;
  • Read operations and write operations are mutually exclusive, which means that write operations and read operations cannot exist at the same time.

Since this is needed, let’s take a look at what a distributed read and write lock is.

What is a distributed read and write lock

Everyone is definitely familiar with locks. Locks are common in golang and are generally used to concurrent access to resources in single-node and multiple goroutines; however, in distributed scenarios, the method of locking a single-node will lose its effect, so in order to achieve mutually exclusive access to shared resources in a distributed environment, people have implemented various distributed locks.

Distributed read and write locks are locks with smaller granularity than distributed locks, and locking in business scenarios will be more flexible. Distributed read and write locks also follow the principle of read and write locks:

  • Read mode sharing, write mode mutually exclusive.
  • It has three mode states:Read locked state, write locked state, and no locked state.

The access principle of distributed read and write locks is similar to that of read and write locks. Let's take a look at it in detail below.

Access principles of distributed read and write locks

The following list is the read and write access principle of read and write lock (that is, distributed read and write lock)

Current lock status Read lock request Write lock request
Lockless state Can Can
Read lock status Can Can't
Write lock status Can't Can't

Read lock

  • Read locks can only be acquired under lock-free and read locks.
  • In the read lock mode, any request to read the lock is OK.
  • In the read lock mode, the request for a write lock cannot be obtained until all read locks are unlocked.

Write lock

  • The write lock can only be acquired in the lock-free state.
  • In write lock mode, any requested read lock and write lock are blocked until the write lock is unlocked.

Specific implementation

If there is no mysql database locally, you can quickly build it through this article:How to use docker to build a mysql service

Connect mysql via gorm

gorm is a golang orm framework that can be used to quickly connect to databases.The specific code is as follows:

package main

import (
	"fmt"

	"/driver/mysql"
	"/gorm"
	"/gorm/logger"
)
var (
	db *

	dbUsername = "kele"
	dbPassword = "baishi2020"
	dbHost     = "127.0.0.1:7306"
	dbDatabase = "lingmo"

	stateReadLock  = "ReadLock"
	stateWriteLock = "WriteLock"
	stateUnlock    = "Unlock"
)

type RWLock struct {
	LockMark      string `gorm:"default:'Unlock'"`
	ReadLockCount uint32 `gorm:"default:0"`
	LockReason    string
}

type Stock struct {
	
	RWLock
	Count int64
}

func (Stock) TableName() string {
	return "stocks"
}

func init() {
	dsn := ("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUsername, dbPassword, dbHost, dbDatabase)

	mysqlConfig := {DSN: dsn}
	gormConfig := &{Logger: ()}

	var err error
	if db, err = ((mysqlConfig), gormConfig); err != nil {
		panic(err)
	}

	("db:table_options", "ENGINE = InnoDB DEFAULT CHARSET = utf8")

	// register tables
	if err = (&Stock{}); err != nil {
		panic(err)
	}
}

func main() {
	if result := (&Stock{}).Save(&Stock{Model: {}, RWLock: RWLock{}, Count: 10});  != nil {
		panic()
	}
}

First we define a stocks and add three fields related to read and write locks to it. The meaning of the three fields is as follows:

  • LockMark: It indicates that a certain piece of data is locked, and can only be one of the read lock, write lock, and lock-free states.
  • ReadLockCount: First, the read mode is shared, meaning that there can be multiple goroutines concurrent accesses, while the ReadLockCount field records the number of goroutines currently being accessed concurrently.
  • LockReason: Record the reason for the current lock; the read lock is the lockReason of the latest goroutine, while the write lock is the lockReason of the write lock goroutine.

The rest are some gorm connection mysql logic, which I will not elaborate on here.

Implement read lock mode

The specific code is as follows:

func (s Stock) RLock(db *, lockReason string) error {
	condition := "(id = ?) AND (lock_mark != ?)"
	fields := map[string]interface{}{
		"lock_mark":       stateReadLock,
		"read_lock_count": ("read_lock_count + ?", 1),
		"lock_reason":     lockReason,
	}

	result := (&Stock{}).Where(condition, , stateWriteLock).Updates(fields)
	if  != nil {
		return 
	}
	if  == 0 {
		return ("failed to rlock Stock, RowsAffected=0")
	}

	return nil
}

func (s Stock) RUnlock(db *, UnLockReason string) error {
	sql := (`UPDATE stocks SET read_lock_count=if(read_lock_count>0,read_lock_count-1,0), lock_mark=if(read_lock_count<1, 'Unlock', 'ReadLock'),lock_reason ='%s' where id= %d and lock_mark='%s'`, UnLockReason, , stateReadLock)
	result := (sql)
	if  != nil {
		return 
	}
	if  == 0 {
		return ("failed to RUnlock Stock, RowsAffected=0")
	}

	return nil
}

func main() {
	if result := (&Stock{}).Save(&Stock{Model: {}, RWLock: RWLock{}, Count: 10});  != nil {
		panic()
	}

	s := &Stock{Model: {ID: 1}}
	if result := (s).First(s);  != nil {
		panic()
	}
	if err := (db, "readLock_reason_1"); err != nil {
		panic(err)
	}
	if err := (db, "readLock_reason_2"); err != nil {
		panic(err)
	}

	if err := (db, "readLock_unlock_1"); err != nil {
		panic(err)
	}
	if err := (db, "readLock_unlock_2"); err != nil {
		panic(err)
	}
}

Executing the above code can run normally. Let’s analyze it below:

  • The sql statement for reading locks is as follows, and the read lock can be added as long as it is not written locked.
UPDATE `stocks` SET `lock_mark` = 'ReadLock', `lock_reason` = 'readLock_reason_1', `read_lock_count` = read_lock_count + 1, `updated_at` = '2022-09-25 14:58:45.693' WHERE (( id = 1 ) 
AND ( lock_mark != 'WriteLock' )) 
AND `stocks`.`deleted_at` IS NULL
  • The SQL statements for interpreting locks are as follows. The lock can only be interpreted in the read lock state. In addition, the read_lock_count and lock_reason fields must be updated.
UPDATE stocks 
SET read_lock_count =
IF
    ( read_lock_count > 0, read_lock_count - 1, 0 ),
    lock_mark =
IF
    ( read_lock_count < 1, 'Unlock', 'ReadLock' ),
    lock_reason = 'readLock_unlock_1' 
WHERE
    id = 1 
    AND lock_mark = 'ReadLock'

Implement write lock mode

The specific code is as follows:

func (s Stock) WLock(db *, lockReason string) error {
	condition := "(id = ?) AND (lock_mark = ?)"
	fields := map[string]interface{}{
		"lock_mark":       stateWriteLock,
		"read_lock_count": 0,
		"lock_reason":     lockReason,
	}
	result := (&Stock{}).Where(condition, , stateUnlock).Updates(fields)
	if  != nil {
		return 
	}
	if  == 0 {
		return ("failed to WLock Stock, RowsAffected=0")
	}

	return nil
}

func (s Stock) WUnlock(db *, UnLockReason string) error {
	condition := "(id = ?) AND (lock_mark = ?)"
	fields := map[string]interface{}{
		"lock_mark":       stateUnlock,
		"read_lock_count": 0,
		"lock_reason":     UnLockReason,
	}

	result := (&Stock{}).Where(condition, , stateWriteLock).Updates(fields)
	if  != nil {
		return 
	}
	if  == 0 {
		return ("failed to WUnlock Stock, RowsAffected=0")
	}

	return nil
}

func main() {
	s := &Stock{Model: {ID: 1}}
	if result := (s).First(s);  != nil {
		panic()
	}
	if err := (db, "writeLock_reason_1"); err != nil {
		panic(err)
	}
	if err := (db, "unWriteLock_reason_1"); err != nil {
		panic(err)
	}
}

Execute the above code and run it. The following are the analysis results

  • The sql statement for writing locks is as follows. Only when the lock is not locked can the lock be successfully added.
UPDATE `stocks` SET `lock_mark` = 'WriteLock', `lock_reason` = 'writeLock_reason_1', `read_lock_count` = 0, `updated_at` = '2022-09-25 15:06:10.71' WHERE (( id = 1 ) 
AND ( lock_mark = 'Unlock' )) 
AND `stocks`.`deleted_at` IS NULL
  • The SQL statement for unlocking is as follows. Only in the write lock state can the lock be unlocked.
UPDATE `stocks` SET `lock_mark` = 'Unlock', `lock_reason` = 'unWriteLock_reason_1', `read_lock_count` = 0, `updated_at` = '2022-09-25 15:06:10.719' WHERE (( id = 1 ) 
AND ( lock_mark = 'WriteLock' )) 
AND `stocks`.`deleted_at` IS NULL

Summarize

There are many ways to implement distributed read and write locks, which can also be implemented through etcd and redisson. This article emphasizes that it can be implemented through mysql. The advantage of this method is that it does not have to introduce additional components and is relatively simple to implement, so there are certain application scenarios.

This is the article about golang simply implementing distributed read and write locks based on mysql. For more related golang read and write locks, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!