whutjiajiao 2019-11-05
源码来自https://github.com/study-only/go-locks
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命令看出,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() }
唯一索引是一种索引,它不允许具有索引值相同的行,从而禁止重复的索引或键值。该特性类似于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...)) }