我们常用的synchronized,lock等都是jvm层面的锁,在分布式环境中是无法实现加锁的;常用的分布式锁实现方案有几种:
数据库乐观锁:在表中增加一个version字段,每次更新数据的时候,先获取当前的version,更新的时候对比数据库中的version跟获取的version是否相同;如果不同,则更新失败;算是一种cas操作,每次更新数据的时候加上verson判断;
数据库悲观锁:在更新数据的时候使用select for update;select 的记录行会被锁中,直到事务执行完成才会释放,所以需要执行在事务中;
zookeeper实现分布式锁,主要是使用zk的临时节点的有序性;首先每个线程会在某个目录下创建一个顺序性节点,然后获取这个目录下的所有节点,判断当前的节点,是否是最小节点,如果是,获取到锁;如果是次小节点,则监听比自己小的节点变化,之后重新尝试比较判断自己是否是最小的,是则获取到锁;zk使用curator来操作;
setnx命令,其实如果某个key不存在的话,设置某个key的value;如果存在,则返回失败;
127.0.0.1:6379> SETNX test 'try'
(integer) 1
127.0.0.1:6379> get test
"try"
127.0.0.1:6379> SETNX test 'tryAgain'
(integer) 0
127.0.0.1:6379> get test
"try"
可以配合ex或者px设置key的过期时间
#给lock设置了过期时间为60000毫秒(也可以用ex 6000,单位就变成了秒),当用NX再次赋值,则返回nil,不能重入操作
127.0.0.1:6379> set lock true NX px 60000
OK
127.0.0.1:6379> set lock true NX px 6000
(nil)
127.0.0.1:6379> get lock
"true"
127.0.0.1:6379> ttl lock
(integer) 43
#时间过期后再次get,返回nil,表明key 为 lock的锁已经释放
127.0.0.1:6379> get lock
(nil)
存在问题以及解决方案:
1>不设置过期时间,会导致如果应用崩了,那么这个锁永远无法释放;
2>如果设置了过期时间,那么到了过期时间,业务操作A还没有结束,会导致其他的线程B获取到锁;然后A操作完了之后,直接释放锁,这个时候,等于是把B的锁释放了;解决办法,在释放的时候,判断key对应的value是不是自己的就可以了,如果不是自己的,就不处理;
另外还有几个问题
不支持重入,如果同个线程,再次获取锁,会失败;
不支持续约,就上面提到的两个问题,锁时间太短会导致业务没操作完,锁释放了;锁时间太长,如果宕机会导致其他的线程再也无法获取锁;
不具备阻塞能力,如果没有获取到锁,只能业务上进行cas操作轮训;这种操作应该放在锁上进行操作,而不是放在业务逻辑上;
而Redission则满足上面的几点;
Redisson使用hash做数据结构,key是业务指定锁名称,field是线程id,value是加锁的次数;
具体代码
依赖
org.redisson
redisson-spring-boot-starter
3.10.6
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 也可以将 redis 配置信息保存到配置文件
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
使用方式
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class LockController {
@Autowired
private RedissonClient redissonClient;
@GetMapping("/lock")
public String lockResource() throws InterruptedException {
String lockKey = "myLock";
// 获取 RLock 对象
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁(尝试加锁)(锁超时时间是 30 秒)
boolean isLocked = lock.tryLock(30, TimeUnit.SECONDS);
if (isLocked) {
// 成功获取到锁
try {
// 模拟业务处理
TimeUnit.SECONDS.sleep(5);
return "成功获取锁,并执行业务代码";
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
} else {
// 获取锁失败
return "获取锁失败";
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return "获取锁成功";
}
}
默认创建的是非公平锁,可以使用下面代码创建公平锁;
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
还可以创建联合锁
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
还可以创建读写锁
redisson.getReadWriteLock("anyRWLock");
RLock lock = rwlock.readLock();
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
// 或
RLock lock = rwlock.writeLock();
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();