如何用 redis 造一把分布式锁

基本概念

wiki:In computer science, a lock or mutex (from mutual exclusion) is a synchronization mechanism for enforcing limits on access to a resource in an environment where there are many threads of execution. A lock is designed to enforce a mutual exclusion concurrency control policy. 在计算机科学中,锁或互斥量是一种同步机制,用于在多线程执行环境中,强行限制对资源访问。锁常被用于同步并发控制。

简单来讲,锁是用来控制多线程执行对资源的并发访问的。比如当一个资源只允许在任意时刻只有一个执行线程对其进行写操作,那当其他线程要访问资源时,就必须要检查该该资源上是否存在写操作锁,如果存在,必须要等待锁的释放并获得锁之后才能对资源进行访问。

悲观锁

悲观锁假设在一个完整事务发生的过程中,总是会有其他线程会更改所操作的资源,因此线程总是对资源加锁之后才会对其做更改。

乐观锁

乐观锁假设在一个完整事务发生的过程中,不一定会有其他线程会更改资源,一旦发现资源被更改,则停止当前事务回滚所有操作。

分布式锁

常见的锁有多线程锁、数据库事务中的行级锁、表级锁等,这些锁的特点都是发生在单一系统环境中,如果需要在不同的进程、不同机器和系统等分布式环境中控制对资源的访问,这时候我们需要一把分布式锁。

简单来说,分布式锁就是解决分布式环境中对资源的访问限制。

如何设计一把分布式锁

我们用 redis 来实现这把分布式的锁,redis 速度快、支持事务、可持久化的特点非常适合创建分布式锁。

分布式环境中如何消除网络延迟对锁获取的影响

锁,简单来说就是存于 redis 中一个唯一的 key。一般而言,redis 用 set 命令来完成一个 key 的设置(加锁),使用 get 命令获取 key 的信息(检查锁)。由于网络延迟的存在,简单的使用 set 和 get 命令可能会带来如下问题:

线程 A 检查锁是否存在(get)–>否–>加锁(set),在 A 发起加锁命令但是还没有加锁成功的时候,可能线程 B 已经完成了 set 操作,锁被 B 获得,但是 A 也发起了加锁请求,由于 set 命令并不检查 key 的存在,B 的锁很可能会被 A 的 set 操作破坏。

幸运的是,redis 提供了另一个命令 setx : 当指定的 key 不存在时,设置 key 的值为指定 value,如果存在,不做任何操作,成功则返回 1,失败则返回 0。也就是只要命令返回成功,线程就能正确获得锁,不需要再做类似 get 检查操作。

使用 setx 可以消除网络延迟对锁设置的影响。

加锁的客户端发生 crash 导致锁不能被正确释放应该怎么处理?

加锁成功并操作完成时候,就需要加锁线程对锁进行释放,以让出资源的控制权。释放锁,简单来说就是删除 redis 中这个唯一的 key,但是一定要保证删除的这个 key 是该线程创建的,因而锁创建时必须携带执行线程的唯一特征以标示创建者的身份。

如果加锁的线程出现异常 crash 了而不能及时删除锁,则会导致锁一直无法被正确释放,资源处于一直被占有,别的线程处于一直等待的状态。为了避免这样的情况发生,锁一定要在异常发生之后可以自己释放,以让出资源的控制权,可以使用 redis 的超时机制来达到这个目的。超时时间视不同的业务场景而定,一般是最大允许等待时间。需要注意的是,只有在加锁成功之后才可以对 key 设置 TTL,否则很容易导致 key 被多个线程不断设置 TTL 而无法过期。

if CONN.setnx(lockname, identifier):
 CONN.expire(lockname, timeout)

加锁之后如何有效监测锁是否被篡改?

redis 提供了 pipeline 和事务操作来保证多个命令可以在一个事务内全部完成从而减少多次网络请求带来的开销,watch 命令又可以在事务开始执行之前对所要操作的 key 执行监测,从而保证了事务的完整性和一致性。因此,为了防止锁篡改,可以在加锁完成之后对锁进行 watch 操作,一旦锁发生变化,则终止事务,回滚操作。

pipe = CONN.pipeline(True)
pipe.watch(lock)

提供锁的宿主机( redis 服务器) crash 导致锁不能被正确建立和释放该如何处理?

不论是通信故障或是服务器故障而导致的锁服务器无法响应,此时都会导致客户端加锁和释放锁的请求无法完成,因此一定要有相应的应急处理,以确保程序流程的完整体验,加强客户端的健壮性。比如相应的超时提示,异常告警等。

哪些边界需要注意

1.只有锁正确释放才算是整个事务的完整结束,如果锁释放失败,比如被篡改、锁服务器异常等,不同的业务可以根据自己的需求进行变动和调整。

2.设置 TTL 一定要是加锁成功之后,否则所有获取锁的客户端都会尝试 TTL 导致锁无法过期。

3.锁的过期时间也就是获取锁的客户端的最大等待时间,这个时间根据执行的事务能够容忍的最长时间为限

一个简单的 python 实现

import time
import redis
import logging
logger = logging.getLogger('service.redis_lock')
CONN = redis.Redis(host='localhost')
def acquire_lock(lockname, identifier, wait_time=20, timeout=15):
 end = time.time() + wait_time
 while end > time.time():
 if CONN.setnx(lockname, identifier):
 CONN.expire(lockname, timeout) # set expire time 
 return identifier
 time.sleep(0.001) #wait until the lock expired or release by some thread
 return False
def release_lock(lockname, identifier):
 pipe = CONN.pipeline(True)
 try:
 #watch lock once lock has been changed, break this transaction
 pipe.watch(lockname)
 #check if lock has been changed
 if pipe.get(lockname) == identifier:
 pipe.multi()
 pipe.delete(lockname)
 pipe.execute()
 return True
 pipe.unwatch() #execu when identifier not equal
 except redis.exceptions.WatchError as e:
 logger.error(e)
 return False
 except Exception as e:
 logger.error(e)
 return False
 return False
if __name__ == '__main__':
 print release_lock('h', 'a')

现在互联网公司面试的时候都会问到Redis,但是仅仅掌握以上所述是不够的,我们需要掌握更多的基础知识,这是我整理的一些需要掌握的知识技术点,分享给大家:

需要思维导图格式的可以加群:810589193免费获取

1. 高性能架构专题

1.1. 分布式架构思维

  • 大型互联网架构演进过程
  • 架构师应具备的分布式知识
  • 主流分布式架构设计详解

1.2. Zookeeper分布式环境指挥官

  • zookeeper基础
  • zookeeper进阶
  • zk的使用举例

1.3. Nginx高并发分流进阶实战

  • Nginx模块简介
  • Nginx工作原理及安装配置
  • Nginx常用命令管理及升级
  • Nginx配置文件精讲
  • 实战线上Nginx多站点配置
  • Nginx配置优化及深入剖析
  • Nginx Rewrite规则剖析
  • Nginx日志分析及脚本编写
  • Nginx日志切割案例讲解
  • Nginx防盗链案例配置
  • Nginx日常运维及故障解决
  • Nginx构建安全HTTPS架构实战
  • 企业实战Nginx+Tomcat动静分离架构实战
  • Nginx+Keepalived集群架构实战
  • Nginx+Keepalived双主架构案例实战

1.4. ActiveMq消息中间件

  • 消息中间件(ActiveMQ、RabbitMQ、Kafka)简介及对比
  • 软件下载、安装及部署
  • P2P、PUBSUB模型详解
  • 消息确认及重发机制
  • 企业级高可用集群部署方案
  • ActiveMQ基于Spring完成分布式消息队列实战

1.5. RabbitMq消息中间件

  • RabbitMQ及高可用集群部署
  • 深入学习RabbitMQ消息分发机制及主题消息分发
  • RabbitMQ消息路由机制分析
  • RabbitMQ消息确认机制分析
  • RabbitMQ基于Spring完成分布式消息队列实战
  • 安装配置
  • 集群化与镜像队列

1.6. Kafka百万级吞实战

  • 基于ZooKeeper搭建高可用集群实战
  • Kafka消息处理过程解析
  • 基于Java语言实现Kafka生产者与消费者实例
  • Kafka副本机制及选举原理窥探
  • 使用Kafka实现日志实时上报统计分析实战

1.7. Memcached进阶实战

  • 概述
  • 开发基础

1.8. Redis高性能缓存数据库

  • Redis初入门及介绍
  • Redis主从模式
  • Redis常用命令及应用场景
  • Redis客户端
  • Redis持久化
  • 哨兵核心机制
  • 高可用集群
  • 原子性
  • 应用场景代码开发与设计分析实战

1.9. MongoDB进阶实战

  • mongodb入门
  • mongodb进阶
  • mongodb高级知识
  • 最佳实践与注意事项

1.10. 高性能缓存开发实战

  • 缓存雪崩解决方案实战
  • 缓存粒度控制实战
  • 缓存击穿实战
  • 缓存热点KEY重建优化实战
  • 缓存同步实战
  • Spring-Cache开发实战

1.11. Mysql高性能存储实战

  • Mysql
  • Mycat

1.12. FastDFS分布式文件存储实战

  • 文件存储实战
  • 文件同步实战
  • 文件查询实战
  • 分布式部署实战

1.13. 高并发场景分布式解决方案实战

  • 分布式主键生成方案
  • Session跨域共享实战
  • 分布式事务解决方案实战
  • 分布式锁解决方案实战
  • 分布式单点登录 SSO实战
  • 分布式调度任务实战
  • 分布式配置中心

针对以上的技术点,有十余年Java经验系统架构师、项目经理录制了一些视频,用来回答这些技术。

最后送福利了,现在加群:810589193可以免费获取Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析等多个知识点高级进阶干货的相关视频资料,还有spring和虚拟机等书籍扫描版,还有更多面试题等你来拿

分享给喜欢Java,喜欢编程,有梦想成为架构师的程序员们,希望能够帮助到你们

​​

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券