[golang] 使用mysql及redis实现简单的分布式锁
source link: https://www.tuicool.com/articles/aaayeyF
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
源码来自 https://github.com/study-only/go-locks
Redis分布式锁
Redis SET
命令
从 Redis 2.6.12
版本开始, SET
命令的行为可以通过一系列参数来修改,详见 Redis命令参考
:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
可选参数
-
EX seconds
:将键的过期时间设置为seconds
秒。 执行SET key value EX seconds
的效果等同于执行SETEX key seconds value
。 -
PX milliseconds
:将键的过期时间设置为milliseconds
毫秒。 执行SET key value PX milliseconds
的效果等同于执行PSETEX key milliseconds value
。 -
NX
: 只在键不存在时, 才对键进行设置操作。 执行SET key value NX
的效果等同于执行SETNX key value
。 -
XX
: 只在键已经存在时, 才对键进行设置操作。
返回值
在 Redis 2.6.12
版本以前, SET
命令总是返回 OK 。
从 Redis 2.6.12
版本开始, SET
命令只在设置操作成功完成时才返回 OK
; 如果命令使用了 NX
或者 XX
选项, 但是因为条件没达到而造成设置操作未执行, 那么命令将返回空批量回复(NULL Bulk Reply)
Redis分布式锁实现原理
从Redis命令看出, SET
命令为原子操作,我们可以用 SET key value EX seconds NX
来实现分布式锁,下面为go语言实现:
import ( "errors" "time" "github.com/go-redis/redis" ) var redisClient *redis.Client type redisLock struct { name string expiry time.Duration } func (l *redisLock) TryLock() error { if ok, _ := redisClient.SetNX(l.name, 1, l.expiry).Result(); !ok { return errors.New("redis lock: already locked") } return nil } func (l *redisLock) Unlock() error { return redisClient.Del(l.name).Err() }
MySQL分布式锁
唯一索引是一种索引,它不允许具有索引值相同的行,从而禁止重复的索引或键值。该特性类似于Redis的使用了 NX
参数的 SET
命令,我们也可以将它用来实现锁。但可能会有MySQL性能压力,需要谨慎使用。
import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" "time" ) const ( createTableSql = ` CREATE TABLE IF NOT EXISTS %s ( id int NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, expire_at timestamp NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_name (name) USING HASH, KEY idx_expire_at (expire_at) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ` insertRowSql = ` INSERT INTO %s (name, expire_at, created_at) VALUES (?, ?, ?) ` deleteRowSql = ` DELETE FROM %s WHERE name=? LIMIT 1 ` ) var lockDb *sql.DB var lockTableName string type mysqlLock struct { name string expiry time.Duration } func (l *mysqlLock) TryLock() error { createdAt := time.Now() expireAt := createdAt.Add(l.expiry) return insertRow(l.name, expireAt, createdAt) } func (l *mysqlLock) Unlock() error { return deleteRow(l.name) } func createTable() error { query := fmt.Sprintf(createTableSql, lockTableName) _, err := lockDb.Exec(query) return err } func insertRow(name string, expireAt, createdAt time.Time) error { query := fmt.Sprintf(insertRowSql, lockTableName) _, err := lockDb.Exec(query, name, expireAt, createdAt) return err } func deleteRow(name string) error { query := fmt.Sprintf(deleteRowSql, lockTableName) _, err := lockDb.Exec(query, name) return err }
自旋锁
上面实现的 MySQL
和 Redis
分布式锁都是非阻塞的,如果要实现阻塞功能,还需升级成自旋锁。下面给出了自旋锁的实现:
import ( "errors" "fmt" "time" ) type TryLocker interface { TryLock() error Unlock() error } func NewSpinLock(lock TryLocker, spinTries int, spinInterval time.Duration) *spinLock { return &spinLock{ lock: lock, spinTries: spinTries, spinInterval: spinInterval, } } type spinLock struct { lock TryLocker spinTries int spinInterval time.Duration } func (l *spinLock) Lock() error { for i := 0; i < l.spinTries; i++ { if err := l.lock.TryLock(); err == nil { return nil } time.Sleep(l.spinInterval) } return errorf("spin lock: failed after %f seconds", float64(l.spinTries)*l.spinInterval.Seconds()) } func (l *spinLock) Unlock() error { return l.lock.Unlock() } func errorf(format string, args ...interface{}) error { return errors.New(fmt.Sprintf(format, args...)) }
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK