golang 分布式锁
参考java redisson包写的golang版分布式锁,使用redigo。实现了重入功能,锁等待采用redis发布/订阅功能,redisson续期功能暂未实现,经过一定的本地测试,有一定的可用性
package main
import (
"context"
"crypto/rand"
"errors"
"fmt"
_ "net/http/pprof"
"sync"
"time"
)
import redigo "github.com/gomodule/redigo/redis"
const LOCK_SCRIPT = `if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);`
const SUBSCRIBE_PRE = "msg_"
const RENEWAL_TIME = 10
const RENEWAL_SCRIPT = "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;"
const UNLOCK_SCRIPT = "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;"
type RedisLock struct {
LockName string
Timeout int64
TimeWait int64
LockId string
}
var pool *redigo.Pool
var mut sync.Mutex
var group sync.WaitGroup
var t int
func main() {
}
func test(name string,j *int){
c := pool.Get()
a := RedisLock{
LockName: "zp",
LockId: "",
Timeout: 60000,
TimeWait: 50000,
}
a.Lock(c)
for i := 0; i < 100000; i++ {
*j++
}
a.Lock(c)
for i := 0; i < 100000; i++ {
*j++
}
a.UnLock(c)
for i := 0; i < 100000; i++ {
*j++
}
a.UnLock(c)
c.Close()
t--
group.Done()
}
func PoolInitRedis(server string, password string) *redigo.Pool {
return &redigo.Pool{
MaxIdle: 20,
IdleTimeout: 240 * time.Second,
MaxActive: 200,
Dial: func() (redigo.Conn, error) {
c, err := redigo.Dial("tcp", server)
if err != nil {
return nil, err
}
if password != "" {
if _, err := c.Do("AUTH", password); err != nil {
c.Close()
return nil, err
}
}
return c, err
},
TestOnBorrow: func(c redigo.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
}
func (r *RedisLock) Lock(conn redigo.Conn) (bool, error) {
if r.LockId == "" {
err := r.GetUuid()
if err != nil {
return false,err
}
}
if r.TimeWait == 0 {
do, err := conn.Do("EVAL", LOCK_SCRIPT, 1, r.LockName, r.Timeout, r.LockId)
if err != nil {
return false, err
}
if do == nil {
return true, nil
}
if r.TimeWait == 0 {
return false, nil
}
}
currency := time.Now().UnixMilli()
last := currency + r.TimeWait
for time.Now().UnixMilli() - r.TimeWait <currency {
time.Sleep(100*time.Microsecond)
do, err := conn.Do("EVAL", LOCK_SCRIPT, 1, r.LockName, r.Timeout, r.LockId)
if err != nil {
return false, err
}
if do == nil {
return true, nil
}
if r.TimeWait == 0 {
return false, nil
}
r.SubscribeUnlock(conn,last-time.Now().UnixMilli())
}
return false, errors.New("time out")
}
func (r *RedisLock) GetUuid() (error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return err
}
r.LockId = fmt.Sprintf("%x-%x-%x-%x-%x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
return nil
}
func (r *RedisLock) SubscribeUnlock(conn redigo.Conn, timeout int64) (bool, error) {
if timeout <= 0 {
return false,nil
}
ctx, cancelFunc := context.WithTimeout(context.Background(), (time.Millisecond)*time.Duration(timeout))
defer cancelFunc()
done := make(chan error, 1)
psc := redigo.PubSubConn{
Conn: pool.Get(),
}
if err := psc.Subscribe(SUBSCRIBE_PRE + r.LockName); err != nil {
return false, err
}
go func() {
for {
switch msg := psc.Receive().(type) {
case error:
fmt.Println(msg)
done <- fmt.Errorf("redis pubsub receive err: %v", msg)
return
case redigo.Message:
done <- nil
return
case redigo.Subscription:
if msg.Count == 0 {
done <- errors.New("all channels are unsubscribed")
return
}
}
}
}()
tick := time.NewTicker(time.Second)
defer tick.Stop()
for {
select {
case <-ctx.Done():
if err := psc.Unsubscribe(); err != nil {
return false,err
}
return false,errors.New("timeout,unsubscribe")
case err := <-done:
if err != nil {
return false,err
}else {
return true,nil
}
case <-tick.C:
if err := psc.Ping(""); err != nil {
return false,errors.New("subscribe heart error")
}
default:
i, err := redigo.Int(conn.Do("exists", r.LockName))
if err == nil&&i == 0 {
return true,nil
}
}
}
return false, nil
}
func (r *RedisLock) UnLock(conn redigo.Conn) (bool,error) {
do, err := conn.Do("EVAL", UNLOCK_SCRIPT, 2, r.LockName, SUBSCRIBE_PRE+r.LockName, "unlock", r.Timeout, r.LockId)
if err != nil {
return false,err
}
switch do.(type) {
case int64 :
return true,nil
default:
return false,errors.New("no you lock")
}
return false,nil
}
func (r *RedisLock) LockState(conn redigo.Conn) (bool,error) {
do, err := conn.Do("hget", r.LockName, r.LockId)
if err != nil {
return false,err
}
if do != nil {
return true,nil
}else{
return false,nil
}
}