学习编程初期,咱们作的诸如教务系统、成绩管理系统大可能是单机架构,单机架构在处理并发的问题上通常是依赖于JDK内置的并发编程类库,如synchronize关键字、Lock类等。随着业务以及需求的提升,单机架构再也不知足咱们的要求,这个时候咱们难免要进行业务上的分离,例如基于Maven进行多模块开发。业务与业务分离以后,遇到的首要问题就是业务之间如何进行通讯,相信会有很多读者了解诸如Dubbo、SpringCloud之类的RPC框架,但这些RPC框架并无自带处理分布式并发问题的功能,因此,分布式并发问题还须要咱们本身去实现分布式锁。html
为了确保分布式锁可用,咱们至少要确保锁的实现同时知足如下四个条件:java
分布式锁通常有三种实现方式:git
下面我按个提一下这三种方式的大体实现思路。github
数据库乐观锁的实现方式是先使用SELECT语句查询某字段的值(版本号),该字段即理解为要获取的分布式锁。而后在使用UPDATE语句对正常业务数据进行更新,在UPDATE语句执行时必定要用WHERE条件对版本号进行判断,若版本号在这段时间内并无发生变化则该语句默认执行成功,不然循环执行便可。redis
示例代码:算法
select (status,version) from goods where id=#{id} update goods set status=2,version=version+1 where id=#{id} and version=#{version};
基于Zookeeper实现分布式锁的算法思路大体以下假设锁空间的根节点为/lock:spring
基于Redis的分布式锁实现是基于Redis自带的 setnx 命令。该命令只有在要设置的字段不存在的状况下才能设置成功,也就是得到分布式锁,不然失败。为了防止客户端异常致使的锁未释放问题,还须要对该字段设置过时时间。sql
本文将基于Redis分布式锁的实现思路设计一个spring-boot-starter-redis-lock框架。数据库
核心代码以下:编程
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.concurrent.TimeUnit; @Component public class RedisLock { @Autowired private StringRedisTemplate template; @Autowired private DefaultRedisScript<Long> redisScript; private static final Long RELEASE_SUCCESS = 1L; private long timeout = 3000; public boolean lock(String key, String value) { //执行set命令 Boolean absent = template.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);//1 //其实不必判NULL,这里是为了程序的严谨而加的逻辑 if (absent == null) { return false; } //是否成功获取锁 return true; } public boolean unlock(String key, String value) { //使用Lua脚本:先判断是不是本身设置的锁,再执行删除 Long result = template.execute(redisScript, Arrays.asList(key,value)); //返回最终结果 return RELEASE_SUCCESS.equals(result); } public void setTimeout(long timeout) { this.timeout = timeout; } @Bean public DefaultRedisScript<Long> defaultRedisScript() { DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>(); defaultRedisScript.setResultType(Long.class); defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end"); return defaultRedisScript; } }
执行上面的setIfAbsent()方法就只会致使两种结果:1. 当前没有锁(key不存在),那么就进行加锁操做,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不作任何操做。
回顾上面提到的分布式锁的四个条件,在任意时刻,该代码都能保证只有一个客户端能持有锁,而且每个分布式锁都加了过时时间,保证不会出现死锁,容错性暂时不考虑的话,加锁和解锁经过key保证了对多个客户端而言都是同一把锁,value的做用则是保证对同一把锁的加锁和解锁操做都是同一个客户端。
为了理解咱们想要提升的究竟是什么,咱们先看下当前大多数基于Redis的分布式锁三方库的现状。 用Redis来实现分布式锁最简单的方式就是在实例里建立一个键值,建立出来的键值通常都是有一个超时时间的(这个是Redis自带的超时特性),因此每一个锁最终都会释放(参见前文属性2)。而当一个客户端想要释放锁时,它只须要删除这个键值便可。 表面来看,这个方法彷佛很管用,可是这里存在一个问题:在咱们的系统架构里存在一个单点故障,若是Redis的master节点宕机了怎么办呢?有人可能会说:加一个slave节点!在master宕机时用slave就好了!可是其实这个方案明显是不可行的,由于这种方案没法保证第1个安全互斥属性,由于Redis的复制是异步的。 总的来讲,这个方案里有一个明显的竞争条件(race condition),举例来讲:
固然,在某些特殊场景下,前面提到的这个方案则彻底没有问题,好比在宕机期间,多个客户端容许同时都持有锁,若是你能够容忍这个问题的话,那用这个基于复制的方案就彻底没有问题,不然的话我仍是建议你对上述方案进行改进。好比,考虑使用Redlock算法。
在分布式版本的算法里咱们假设咱们有N个Redis master节点,这些节点都是彻底独立的,咱们不用任何复制或者其余隐含的分布式协调算法。咱们已经描述了如何在单节点环境下安全地获取和释放锁。所以咱们理所固然地应当用这个方法在每一个单节点里来获取和释放锁。在咱们的例子里面咱们把N设成5,这个数字是一个相对比较合理的数值,所以咱们须要在不一样的计算机或者虚拟机上运行5个master节点来保证他们大多数状况下都不会同时宕机。一个客户端须要作以下操做来获取锁:
本文代码仓库:https://github.com/yueshutong/spring-boot-starter-redis-lock
参考文章:https://www.cnblogs.com/ironPhoenix/p/6048467.html