好久之前学的分布式锁,在同学提起后发现已经忘了差不多了,趁着周末没事干复习一下
一、使用Redis
redis使用乐观锁,用watch监控一个或者多个key的值,如果在事务(exec)执行之前,key的值发生变化则会取消事务执行。(multi开启事务,exec关闭事务)但是使用乐观锁会发现性能明显降低,但是redis是nosql,就是为了快捷方便而使用,所以不推荐使用乐观锁。接下来引入分布式锁。
1.简单的分布式锁
首先先了解redis中要用到的指令:setnx key value (当且仅当key的值不存在时,设置key的值为value) 在java中,setnx对应RedisTemplate.opsForValue().setIfAbsent(key,value)。
获取值和设置值在java中分别是RedisTemplate.opsForValue().get(key)、RedisTemplate.opsForValue().set(key,value)
接下来介绍一下简单的思路:在方法中一直使用setIfAbsent在redis中申请创建键值对,当创建成功时,相当于当前方法获取到了锁,接下来即可对共享资源进行操作,最后释放锁资源(即创建的键值对)让其他方法去竞争。
public void deduct(){
String uuid = UUID.randomUUID().toString();
//加锁
while(!this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS)){
try{
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
//查询库存
String stock = redisTemplate.opsForValue().get("stock").toString();
//判断库存充足
if (stock != null && stock.length() != 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
// 扣减库存
redisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
}
}finally {
// redisTemplate.delete("lock"); 会导致误删
if(StringUtils.equals(this.redisTemplate.opsForValue().get("lock"),uuid))
{
this.redisTemplate.delete("lock");
}
}
}
可以看到我们在创建锁的时候添加了3分钟的存活时间,目的是为了防止锁在创建后任务未完成但是服务器宕机等导致锁一直释放不了,导致的死锁问题。同时,在while中添加休眠时间,可以防止方法一直申请导致太过于频繁使得性能下降。
但是添加存活时间会导致任务还未完成,但是锁已经过期了,这时虽然本方法还没执行到释放锁操作,但是其他的方法会获取到锁。等到本方法执行释放锁的时候,可能释放的是别正在执行的方法的锁,导致误删锁,最后导致锁失效。因此我们使用uuid记录当前锁所属的方法防止别的方法误删,但是存在原子性问题,所以我们使用Lua脚本释放锁。
我们先了解一下Lua脚本在redis中编写的方式:
eval “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 4 10 20 30 40 50 60 70 80 90
"4"是用来表示KEYS[]参数的个数 ARGV[]代表着附加参数。
在java中用redisTemplate.execute(new DefaultRedisScript<>(脚本,返回值类型)
,key集合,argv集合)来实现脚本语句
public void deduct(){
String uuid = UUID.randomUUID().toString();
//加锁
while(!this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS)){
try{
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
//查询库存
String stock = redisTemplate.opsForValue().get("stock").toString();
//判断库存充足
if (stock != null && stock.length() != 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
// 扣减库存
redisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
}
} finally {
// //判断是否自己的锁 解锁
String script = "if redis.call('get',KEYS[1]) == ARGV[1] "+
"then " +
" return redis.call('del', KEYS[1]) " +
"else "+
" return 0 "+
"end";
this.redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList("lock"),uuid);
}
}
在实际开发中,这种锁有可能会出现死锁情况,如当A获取锁的顺序为lock01、lock02,而B获取锁的顺序为lock02、lock01,那么在A、B分别获取lock01和lock02后进入死锁,这时候就需要使用可重入锁了。
2.使用redis制作可重入锁
什么是可重入锁:可重入锁的定义就是你得到了当前对象的锁后可以在锁中再次进入带有锁的方法
我们回忆一下可重入锁的使用场景
package com.leolee.multithreadProgramming.juc.reentrantLock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName Test
* @Description: JUC ReentrantLock测试
* 相对于 synchronized,ReentrantLock具有如下特点:
* 1.可中断(一个线程可以取消另一个线程的锁)
* 2.可以设置超时时间
* 3.可以设置为公平锁
* 4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
* 5.与synchronized一样,都支持锁重入
*
**/
@Slf4j
public class Test {
private static final ReentrantLock reentrantLock = new ReentrantLock();
//============================基本使用方法以及可重入测试============================
public void normalTest() {
reentrantLock.lock();
try {
//临界区,需要被保护的代码块
log.info("main method 获得锁,开始执行");
this.m1();
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void m1() {
reentrantLock.lock();
try {
//临界区,需要被保护的代码块
log.info("m1 获得锁,开始执行");
this.m2();
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void m2() {
reentrantLock.lock();
try {
//临界区,需要被保护的代码块
log.info("m2 获得锁,开始执行");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) {
Test test = new Test();
test.normalTest();
}
}
执行结果:
20:33:20.368 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - main method 获得锁,开始执行
20:33:20.388 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - m1 获得锁,开始执行
20:33:20.388 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - m2 获得锁,开始执行
synchronized和reentrantlock都是重入锁,例子可以查看可重入锁详解(什么是可重入)_石头wang的博客-CSDN博客_可重入锁
我们模仿ReentrantLock来建造一个redis的分布式锁
首先,我们先了解reentrantLock的加锁流程:ReentrantLock.Lock() /申请锁-->NonfairSync.lock() /非公平锁 -->AQS.acquire(1) /进入AQS中申请锁 -->NonfairSync.tryAcquire(1) / 非公平锁申请锁-->Sync.nonfairTryAcquire(1) /进入Sync的非公平锁尝试获取
如果state=0,则代表没有线程占用锁,则获取锁就成功,进行cas(compareAndSet)操作将state设置为1;如果state大于0,则让state加上1,意味着已经重入获取锁了。否则请求失败。
解锁流程:ReentrantLock.unlock() --> AQS.release(1) --> Sync.tryRelease(1)
1.判断当前线程是否为有锁线程,若没锁则抛出异常。
2.对state -1操作,若变成0,则说明解锁成功,返回true ;若不为0,贼返回false。
实现
我们还是使用lua脚本来实现获取锁与释放锁。
根据上面获取的思路,我们可以设计这样的加锁流程:
1.判断锁是否存在(exists),不存在则获取锁 hset key field value (相当于设置hashmap key的键值对)
2 如果锁存在则判断是否是自己的锁,是自己的锁就重入 hincrby key field increment
3 如果锁存在且不是自己的锁 则循环重试
在操作后发现 hincrby也能实现hset的功能 所以我们合并1和2
"if redis.call('exits',KEY[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
"then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end"
解锁流程:
1.不存在的话返回null
2.存在的话先进行-1操作 如果等于零则说明释放完成就删除锁
3.-1后不为零则返回0
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) ==0 "+
"then "+
" return nil " +
" elseif redis.call('hincrby', KEYS[1], ARGV[1],-1) ==0 " +
" then return redis.call('del',KEYS[1]) " +
"else "+
" return 0 "+
" end";
我们模仿reentrantlock创建一个工具类distributedlock,实现lock接口。
public class DistributedRedisLock implements Lock {
@Autowired
private StringRedisTemplate redisTemplate;
public String lockName;
private String uuid;
private Long expire = Long.valueOf(30);
public DistributedRedisLock(StringRedisTemplate redisTemplate, String lockName,String uuid) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuid = uuid+ ":"+Thread.currentThread().getId();
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
return this.tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
return false;
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time!=-1){
this.expire = unit.toSeconds(time);
}
Map<String,Integer>ma =new HashMap<>();
String script = "if redis.call('exits',KEY[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
"then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
while(!this.redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),
Arrays.asList(lockName),
uuid,
String.valueOf(expire))) {
Thread.sleep(50);
}
//加锁
this.renewExpire();
return true;
}
@Override
public void unlock() {
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) ==0 "+
"then "+
" return nil " +
" elseif redis.call('hincrby', KEYS[1], ARGV[1],-1) ==0 " +
" then return redis.call('del',KEYS[1]) " +
"else "+
" return 0 "+
" end";
Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
if (flag == null){
throw new IllegalMonitorStateException("this lock doesn't belong to you!");
}
}
@Override
public Condition newCondition() {
return null;
}
private void renewExpire(){
String script ="if redis.call('hexists', KEYS[1], ARGV[1]) == 1" +
"then "+
" return redis.call('expire', KEYS[1], ARGV[2]) "+
" return 0 "+
" end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(expire))) {
renewExpire();
}
}
},this.expire*1000/3);
}
}
注意 :我们使用renewExpire函数来自动续期
使用工厂模式来创建distributedlock
@Component
public class DistributedLockClient {
@Autowired
private StringRedisTemplate redisTemplate;
private String uuid;
public DistributedRedisLock getRedisLock(String name){
return new DistributedRedisLock(redisTemplate,name,uuid);
}
public DistributedLockClient(){
this.uuid = UUID.randomUUID().toString();
}
}
最终实现方法
public void deduct(){
DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lock");
redisLock.lock();
try {
//查询库存
String stock = redisTemplate.opsForValue().get("stock").toString();
//判断库存充足
if (stock != null && stock.length() != 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
// 扣减库存
redisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
}
}finally {
redisLock.unlock();
}
}
Redission的底层原理类似于上述过程,而且使用方便,具体可以查看Redisson的使用 - 简书 (jianshu.com)因此不做过多介绍。