分布式锁:不同实现方式实践测评(下)

193 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情

2 分布式锁引入

2.1 分布式锁的概念

先说锁

线程是进程的一个实体,同一进程下的多个线程可以进行资源的共享,多个线程共享一个资源时则会进行资源的竞争进而引发线程异常。

基于此类问题,我们引入锁这个概念,锁,是一种线程中的一种同步机制。通过加锁我们就可以实现对共享资源的互斥访问。

为什么会出现分布式锁?

因为集群环境下,无法避免要把一个项目部署成多个节点,但是数据的一致性导致每个节点访问的数据都是一样的,至此我们可以把每一个项目节点都当做一个线程,整个分布式集群当做一个进程,数据就是多个节点共享的资源,因此难免会引发分布式环境下的多线程问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZDZIYfIB-1669462401396)(一文讲述多种分布式锁性能评测与实践.assets/image-20221123205401035.png)]

2.2 Redis实现分布式锁

package redis_lock

import (
   "github.com/go-redis/redis/v8"
   "github.com/go-redsync/redsync/v4"
   "github.com/go-redsync/redsync/v4/redis/goredis/v8"
   "time"
)

type RLock struct {
   Mutex *redsync.Mutex
}

func NewRLock(key string, expire ...time.Duration) *RLock {
   client := redis.NewClient(&redis.Options{
      Addr: "localhost:6379",
   })
   pool := goredis.NewPool(client)
   rs := redsync.New(pool)
   option := redsync.WithExpiry(time.Second * 5)
   if len(expire) == 1 {
      option = redsync.WithExpiry(expire[0])
   }
   mutex := rs.NewMutex(key, option)
   return &RLock{Mutex: mutex}
}

func (r *RLock) Lock() error {
   return r.Mutex.Lock()
}

func (r *RLock) Unlock() error {
   if ok, err := r.Mutex.Unlock(); !ok || err != nil {
      return err
   }
   return nil
}

2.3 etcd实现分布式锁

package etcd_lock

import (
   "context"
   "fmt"
   "go.etcd.io/etcd/client/v3"
   "go.etcd.io/etcd/client/v3/concurrency"
   "time"
)

type EtcdLock struct {
   Mutex *concurrency.Mutex
}

func NewEtcdLock(key string, expire ...time.Duration) *EtcdLock {
   timeOut := time.Second * 5
   if len(expire) == 1 {
      timeOut = expire[0]
   }
   cli, err := clientv3.New(clientv3.Config{
      Endpoints:   []string{"127.0.0.1:2379"},
      DialTimeout: time.Second * 5,
   })
   if err != nil {
      fmt.Println(err)
   }
   s1, err := concurrency.NewSession(cli, concurrency.WithTTL(int(timeOut/time.Second)))
   if err != nil {
      fmt.Println(err)
   }
   return &EtcdLock{Mutex: concurrency.NewMutex(s1, key)}
}

func (r *EtcdLock) Lock(ctx context.Context) error {
   return r.Mutex.Lock(ctx)
}

func (r *EtcdLock) Unlock(ctx context.Context) error {
   return r.Mutex.Unlock(ctx)
}

2.4 Zookeeper实现分布式锁

package zk_lock

import (
   "fmt"
   "time"

   "github.com/go-zookeeper/zk"
)

type ZkLock struct {
   ZLock *zk.Lock
}

func NewZkLock(key string, expire ...time.Duration) *ZkLock {
   timeOut := time.Second * 5
   if len(expire) == 1 {
      timeOut = expire[0]
   }
   c, _, err := zk.Connect([]string{"127.0.0.1:2181"}, timeOut)
   if err != nil {
      fmt.Println(err)
   }
   lock := zk.NewLock(c, fmt.Sprintf("/zkLock/lock-%s", key), zk.WorldACL(zk.PermAll))
   return &ZkLock{ZLock: lock}
}

func (z *ZkLock) Lock() error {
   return z.ZLock.Lock()
}

func (z ZkLock) Unlock() error {
   return z.ZLock.Unlock()
}

3 实践测评过程与需要注意的问题

以Zookeeper加锁的代码举例:

func RunServer() {
	http.HandleFunc("/buyBook", func(w http.ResponseWriter, r *http.Request) {
		username := r.URL.Query().Get("name")
		buyNum := r.URL.Query().Get("num")
		zkLock := zk_lock.NewZkLock(username, time.Second*3)
		err := zkLock.Lock()
		if err != nil {
			fmt.Errorf("Lock err = %s",err)
			w.Write([]byte("购买失败"))
			return
		}
		defer func() {
			err = zkLock.Unlock()
			if err != nil {
				fmt.Errorf("UnLock err = %s",err)
			}
		}()
		resp := service.BuyBook(username, cast.ToInt64(buyNum))
		_, err = w.Write([]byte(resp))
		if err != nil {
			fmt.Errorf("write err %s", err)
		}
	})
	err := http.ListenAndServe(":8081", nil)
	if err != nil {
		fmt.Errorf("Http run err %s ", err)
	}
}

实践过程就不是很难了,就是一个加锁和解锁的过程,但是要注意的一些问题:

  • 锁过期时间。分布式锁设置过期时间可以确保在未来的一定时间内,无论获得锁的节点发生了什么问题,最终锁都能被释放掉。但是时间也不能过短,防止业务还没有执行完锁就失效了。

  • 锁的全局唯一标识。

  • 锁的合理释放。我们要考虑在业务执行完成或发生异常时锁也能得到释放。

4 结论

经过Jmeter的分析报告,我们汇总成了一张表格:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IpLthqwY-1669462401397)(一文讲述多种分布式锁性能评测与实践.assets/image-20221123185215876.png)]

依照表格我们可以得出结论:

Redis是三者中吞吐量、平均响应时间最优的一种方式,但是相对而言不如Zookeeper更加稳定,etcd在虽然在各个维度都不如Redis和Zookeeper,但是它仍然是一款比较优秀的云原生领域分布式注册中心,在集群环境中,Redis会产生脑裂、主从同步失败等安全问题,etcd则可以很大程度上屏蔽此类问题,所以我们不能只关注表面的数据,同时也要兼顾每个组件背后的原理和安全性。

最后,做一个小总结,分布式锁是一个相对复杂的组件,除了本文所讲述的以外,如果想要更好的使用分布式锁,还需要考虑其背后的诸多问题,比如锁操作的原子性、一致性、可重入性等,这些当然也与不同组件背后的算法相关,由于篇幅有限就没有一一详解,当然除了etcd、Redis、Zookeeper等组件之外,还有许多方式可以实现分布式锁,比如高性能的关系型数据库、MySQL乐观锁等等,都需要我们针对自身的业务进行选择。其实无论是一般的线程锁,还是分布式锁的作用都是一样的,只是作用的范围大小不同。只是范围越大技术复杂度就越大。