前言
上一篇博客我们讲到秒杀问题的一人一单在单机模式下使用synchronized添加悲观锁能解决并发问题。
但是在集群模式下,我们使用悲观锁就无法解决并发问题,因为集群中每个java虚拟机不是共用一个锁,而是每个java虚拟机都拥有属于自己的锁,因而就无法保证在并发模式下的一人一单问题。
怎么解决呢?
那就要引入我们今天的要学习的内容—分布式锁。
我们这里先简要介绍一下分布式锁原理:集群模式悲观锁失效是因为每个JVM有其独自的锁,而集群模式就必须共享同一个锁,因此,我们取消每个JVM中的锁,让所有的JVM都去共用同一个锁监视器,这就是分布式锁的基本思想。
1. 什么是分布式锁
分布式锁:满足分布式系统或者集群模式下多进程可见并且互斥的锁。
它需要满足以下特点:
实现效果:同一时刻只有一个线程能拥有互斥锁,只有该线程释放互斥锁,其他线程才能获得互斥锁的使用权。
2. 分布式锁的实现
这里我们使用redis提供的分布式锁功能。
2.1 基于Redis的分布式锁实现方法
这里我们要明确一个点,因为我们学习redis需要考虑在高并发模式下,很多线程共享资源的问题。资源共享就会涉及进程阻塞问题,因此每一步操作就需要尽量满足原子性,减少进程阻塞导致的并发问题。
因此,我们在获取锁的方式可以修改为如下方式
将设置互斥锁和超时时间同步完成,是为了解决在设置锁的时候发生进程阻塞导致锁未设置超时时间,导致锁无法释放的问题。
OK,接下来我们就根据以上方法设置我们的第一个分布式锁。
2.2 基于redis实现分布式锁的初级版本
-
需求分析
-
梳理流程
-
代码实现
public class SimpleRedisLock implements ILock{
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">String</span> KEY_PREFIX <span class="token operator">=</span> <span class="token string">"lock:"</span><span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">String</span> ID_PREFIX <span class="token operator">=</span> UUID<span class="token punctuation">.</span><span class="token function">randomUUID</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token operator">+</span><span class="token string">"-"</span><span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token class-name">StringRedisTemplate</span> stringRedisTemplate<span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token class-name">String</span> name<span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token class-name">SimpleRedisLock</span><span class="token punctuation">(</span><span class="token class-name">StringRedisTemplate</span> stringRedisTemplate<span class="token punctuation">,</span> <span class="token class-name">String</span> name<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">this</span><span class="token punctuation">.</span>stringRedisTemplate <span class="token operator">=</span> stringRedisTemplate<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>name <span class="token operator">=</span> name<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">tryLock</span><span class="token punctuation">(</span><span class="token keyword">long</span> timeSec<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">//获取线程id</span>
<span class="token class-name">String</span> threadId <span class="token operator">=</span> ID_PREFIX<span class="token operator">+</span><span class="token class-name">Thread</span><span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//获取锁</span>
<span class="token class-name">Boolean</span> success <span class="token operator">=</span> stringRedisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">setIfAbsent</span><span class="token punctuation">(</span>KEY_PREFIX <span class="token operator">+</span> name<span class="token punctuation">,</span> threadId<span class="token punctuation">,</span> timeSec<span class="token punctuation">,</span> <span class="token class-name">TimeUnit</span><span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//返回的是boolean类型,是基本数据类型,而我们返回的Boolean类型是包装类,避免拆箱出现空指针。</span>
<span class="token keyword">return</span> <span class="token class-name">Boolean</span><span class="token punctuation">.</span>TRUE<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>success<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">unLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">//删除锁</span>
stringRedisTemplate<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span>KEY_PREFIX <span class="token operator">+</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
}
这里说明两点
-
我们设置的锁需要满足互斥以及具有超时时间,我们使用setIfAbsent()方法,它不仅满足互斥条件,还能同时设置超时时间,满足需要。
-
拆箱与装箱
1)什么是拆箱
• 拆箱就是自动将包装器类型转换为基本数据类型
• 拆箱调用Integer.intValue方法
2)什么是装箱
• 装箱就是自动将基本数据类型转换为包装器类型
• 装箱调用的Integer.valueOf方法
我们在尝试获取锁的方法中返回值时基本数据类型,而设置锁返回的时包装器类型,如果我们将其直接返回会涉及拆箱和装箱过程,我们在开发过程中要尽量避免拆箱和装箱,因为可能会出现空指针问题。
在秒杀券的一人一单代码实现之前获取我们设置的分布式互斥锁,添加基本的分布式锁,由于后面执行代码可能抛异常,因此需要try/finally释放锁
//添加基本的分布式锁,由于后面执行代码可能抛异常,因此需要try/finally释放锁
SimpleRedisLock redisLock = new SimpleRedisLock(redisTemplate, "order:" + userId);
boolean flag = redisLock.tryLock(5);
if(!flag){
return Result.fail("一人只能购买一张优惠券");
}
<span class="token keyword">try</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">//一人一单</span>
<span class="token keyword">int</span> count <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">eq</span><span class="token punctuation">(</span><span class="token string">"user_id"</span><span class="token punctuation">,</span> userId<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">eq</span><span class="token punctuation">(</span><span class="token string">"voucher_id"</span><span class="token punctuation">,</span> voucherId<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">count</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>count<span class="token operator">></span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
<span class="token keyword">return</span> <span class="token class-name">Result</span><span class="token punctuation">.</span><span class="token function">fail</span><span class="token punctuation">(</span><span class="token string">"一个用户只能购买一个优惠券"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">//当更新时查询的库存大于0时进行库存减一</span>
<span class="token keyword">boolean</span> success <span class="token operator">=</span> seckillVoucherService<span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">setSql</span><span class="token punctuation">(</span><span class="token string">"stock=stock-1"</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">gt</span><span class="token punctuation">(</span><span class="token string">"voucher_id"</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">eq</span><span class="token punctuation">(</span><span class="token string">"stock"</span><span class="token punctuation">,</span> voucher<span class="token punctuation">.</span><span class="token function">getStock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>success<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">return</span> <span class="token class-name">Result</span><span class="token punctuation">.</span><span class="token function">fail</span><span class="token punctuation">(</span><span class="token string">"优惠券已被抢完"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">//6. 创建订单</span>
<span class="token comment">//6.1 设置id</span>
<span class="token class-name">VoucherOrder</span> voucherOrder <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">VoucherOrder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Long</span> voucherOrderId <span class="token operator">=</span> <span class="token class-name">RedisIdWorker</span><span class="token punctuation">.</span><span class="token function">nextId</span><span class="token punctuation">(</span><span class="token string">"voucherOrder"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
voucherOrder<span class="token punctuation">.</span><span class="token function">setId</span><span class="token punctuation">(</span>voucherOrderId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//6.2 设置user_id</span>
voucherOrder<span class="token punctuation">.</span><span class="token function">setUserId</span><span class="token punctuation">(</span>userId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//6.3 设置优惠券id</span>
voucherOrder<span class="token punctuation">.</span><span class="token function">setVoucherId</span><span class="token punctuation">(</span>voucherId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span>voucherOrder<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token class-name">Result</span><span class="token punctuation">.</span><span class="token function">ok</span><span class="token punctuation">(</span>voucherOrderId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{<!-- --></span>
redisLock<span class="token punctuation">.</span><span class="token function">unLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
2.3 改进分布式锁
我们结合下面的图进行分析一下使用上述分布式锁可能发生的情况:
- 当线程1获取redis锁后执行业务,这时业务阻塞,因而导致redis锁超时释放。
- 线程2此时获取到redis锁,执行业务,线程2执行业务过程中,线程1被唤醒,完成业务释放了redis锁,而业务2还在执行。
- 线程3获取redis锁,也开始执行业务,此时,线程2和线程3都在执行业务,我们的redis锁在并发模式下又形同虚设了。
怎么解决呢?
我们想想之前是如何解决超卖问题呢?是不是使用了CAS方法。即扣减库存的时候判断库存是否跟之前查询的库存相等,相等再去扣减库存。
方法类似,即我们在删除锁的时候可以设置一个线程标识,标识此时是哪个线程获取了锁。假如一旦获取锁的线程1阻塞,锁超时释放,此时线程2获取了锁,将锁的标识更换了,线程2执行业务的时候线程1唤醒,想要去释放锁,结果发现锁已经不是自己的,就不去释放。问题就解决了。
ok,我们看一下解决的流程图
我们只需要在删除锁的代码进行修改即可。
public void unLock() {
<span class="token comment">//获取当前线程ID</span>
<span class="token class-name">String</span> currentId <span class="token operator">=</span> stringRedisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>KEY_PREFIX <span class="token operator">+</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//线程标识</span>
<span class="token class-name">String</span> threadId <span class="token operator">=</span> ID_PREFIX<span class="token operator">+</span><span class="token class-name">Thread</span><span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//判断当前线程与线程标识是否相同,相同再进行释放</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>threadId<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>currentId<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">//删除锁</span>
stringRedisTemplate<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span>KEY_PREFIX <span class="token operator">+</span> name<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">//删除锁</span>
<span class="token comment">//stringRedisTemplate.delete(KEY_PREFIX + name);</span>
<span class="token punctuation">}</span>
2.4 基于Lua脚本改善分布式锁
上述改进能否完美解决因线程阻塞而导致的锁错误释放问题呢?
我们不妨来看一下下面这张图。
看不明白没关系,我帮你们读懂一下。
- 首先,线程1尝试获取锁成功,执行业务,执行完成后,尝试去释放锁,当程序判断完成是自己的锁后刚要去执行释放锁操作,诶,真不巧 ,线程被阻塞了,锁又被超时释放了。
- 这时,线程2尝试获取锁成功,执行业务,此时,线程1又醒了,醒了还没完,因为线程1已经判断锁是自己的,因此人家醒了二话不说就把锁给释放了
- 最后,线程3进来了,它也获取锁成功,执行业务,此时线程2和线程3就并行执行业务,我们的梦想又破灭了。。。
怎么解决呢?
我们想想问题的关键是不是因为判断锁和删除锁是分开进行的,因而才会出现线程阻塞问题。那我们能不能将这两步操作改成原子级别的操作呢?
单纯靠java实现可能不太行,这里就要引入我们要讲的主角,Lua脚本。
ok,现在我们基于Lua脚本来改进一下我们的释放锁的逻辑。
Lua脚本
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
public class SimpleRedisLock implements ILock{
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">String</span> KEY_PREFIX <span class="token operator">=</span> <span class="token string">"lock:"</span><span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">String</span> ID_PREFIX <span class="token operator">=</span> UUID<span class="token punctuation">.</span><span class="token function">randomUUID</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token operator">+</span><span class="token string">"-"</span><span class="token punctuation">;</span>
<span class="token comment">//代码开始就加载静态代码块,也就是lua脚本</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">DefaultRedisScript</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">Long</span><span class="token punctuation">></span></span> UNLOCK_SCRIPT<span class="token punctuation">;</span>
<span class="token keyword">static</span> <span class="token punctuation">{<!-- --></span>
UNLOCK_SCRIPT <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">DefaultRedisScript</span><span class="token generics"><span class="token punctuation"><</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
UNLOCK_SCRIPT<span class="token punctuation">.</span><span class="token function">setLocation</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">ClassPathResource</span><span class="token punctuation">(</span><span class="token string">"unlock.lua"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
UNLOCK_SCRIPT<span class="token punctuation">.</span><span class="token function">setResultType</span><span class="token punctuation">(</span><span class="token class-name">Long</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">private</span> <span class="token class-name">StringRedisTemplate</span> stringRedisTemplate<span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token class-name">String</span> name<span class="token punctuation">;</span>
<span class="token keyword">public</span> <span class="token class-name">SimpleRedisLock</span><span class="token punctuation">(</span><span class="token class-name">StringRedisTemplate</span> stringRedisTemplate<span class="token punctuation">,</span> <span class="token class-name">String</span> name<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">this</span><span class="token punctuation">.</span>stringRedisTemplate <span class="token operator">=</span> stringRedisTemplate<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>name <span class="token operator">=</span> name<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">tryLock</span><span class="token punctuation">(</span><span class="token keyword">long</span> timeSec<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">//获取线程id</span>
<span class="token class-name">String</span> threadId <span class="token operator">=</span> ID_PREFIX<span class="token operator">+</span><span class="token class-name">Thread</span><span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//获取锁</span>
<span class="token class-name">Boolean</span> success <span class="token operator">=</span> stringRedisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">setIfAbsent</span><span class="token punctuation">(</span>KEY_PREFIX <span class="token operator">+</span> name<span class="token punctuation">,</span> threadId<span class="token punctuation">,</span> timeSec<span class="token punctuation">,</span> <span class="token class-name">TimeUnit</span><span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//返回的是boolean类型,是基本数据类型,而我们返回的Boolean类型是包装类,避免拆箱出现空指针。</span>
<span class="token comment">/*boolean flag = Boolean.TRUE.equals(success);
return flag;*/</span>
<span class="token keyword">return</span> <span class="token class-name">Boolean</span><span class="token punctuation">.</span>TRUE<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>success<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">unLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">//使用lua脚本执行原子级别的操作,不会因为线程阻塞导致释放锁发生错误。</span>
stringRedisTemplate<span class="token punctuation">.</span><span class="token function">execute</span><span class="token punctuation">(</span>UNLOCK_SCRIPT<span class="token punctuation">,</span>
<span class="token class-name">Collections</span><span class="token punctuation">.</span><span class="token function">singletonList</span><span class="token punctuation">(</span>KEY_PREFIX <span class="token operator">+</span> name<span class="token punctuation">)</span><span class="token punctuation">,</span>
ID_PREFIX<span class="token operator">+</span><span class="token class-name">Thread</span><span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
}
执行lua脚本调用 stringRedisTemplate.execute的API,传递三个参数
经过三个版本的分布式锁迭代,我们设计的锁就基本能够满足我们的使用需求了。