分布式锁
Java中的几种锁解决方案
乐观锁与悲观锁
乐观锁
乐观锁呢,它是假设一个线程在取数据的时候不会被其他线程更改数据,但是在更新数据的时候会校验数据有没有被修改过。它是一种比较交换的机制,简称CAS(Compare And Swap)机制。一旦检测到有冲突产生,比如说数据版本号或者最后更新时间不一致,它就会进行重试,直到没有冲突为止。
在JAVA中乐观锁并没有确定的方法,或者关键字,它只是一个处理的流程、策略。
下面以修改数据库数据为例,使用乐观锁:
- 1、在检索数据,将数据的版本号(version)或者最后更新时间一并检索出来;
- 2、操作员更改数据以后,点击保存,在数据库执行update操作;
- 3、执行update操作时,用步骤1检索出的版本号或者最后更新时间与数据库中的记录作比较;
- 4、如果版本号或最后更新时间一致,则可以更新;
- 5、如果不一致,就不执行更新;
悲观锁
悲观锁与乐观锁恰恰相反,悲观锁从读取数据的时候就显示的加锁,直到数据更新完成,释放锁为止。在这期间只能有一个线程去操作,其他的线程只能等待。在JAVA中,悲观锁可以使用 synchronized关键字或者ReentrantLock类来实现。
synchronized 关键字
synchronized 锁有两种使用方式, 一种是加在方法上,它表示这个方法是加了锁的,当多个线程同时调用这个方法时,只有获得锁的线程才可以执行:
public synchronized void method(){
// 方法体。。。
}
另一种是加在代码块上:
synchronized (对象锁){
……
}
我们将需要加锁的语句都写在 synchronized 块内,而在对象锁的位置,需要填写加锁的对象,它的含义是,当多个线程并发执行时,只有获得你写的这个对象的锁,才能执行后面的语句,其他的线程只能等待。
synchronized块通常的写法是synchronized(this),这个this是当前类的实例,也就是说获得当前这个类的对象的锁,才能执行这个方法,这样写的效果和synchronized方法是一样的。
ReentrantLock 类
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test{
private Lock lock = new ReentrantLock();
public void method(){
lock.lock();
// 业务代码
lock.unlock();
}
}
使用lock.lock()进行加锁,使用lock.unlock()进行解锁,加锁后只会有一个线程能够执行加锁后的业务代码。
公平锁与非公平锁
公平锁与非公平锁是从另一个维度对锁的理解。
公平锁
公平锁故名思意,在多线程的情况下,对待每一个线程都是公平的。多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他线程则在队列里进行排队,A执行完方法后,会从队列里取出下一个线程B,再去执行方法。以此类推,对于每一个线程来说都是公平的,不会存在后加入的线程先执行的情况。
非公平锁
非公平锁与之相反,多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他的线程并没有排队,A执行完方法,释放锁后,其他的线程谁抢到了锁,谁去执行方法。会存在后加入的线程,反而先抢到锁的情况。
ReentrantLock 类的实现
公平锁与非公平锁都在ReentrantLock类里给出了实现,我们看一下ReentrantLock的源码。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock有两个构造方法,默认的构造方法中,sync = new NonfairSync();我们可以从字面意思看出它是一个非公平锁。再看看第二个构造方法,它需要传入一个参数,参数是一个布尔型,true是公平锁,false是非公平锁。
分布式锁
尚文提到的锁,都是有JDK官方提供的锁的解决方案,也就是说这些锁只能在一个JVM进程内有效,我们把这种锁叫做单体应用锁。然而在我们的分布式系统中,这种单体应用锁就无法满足我们需求了,因为分布式系统是由多应用组成,会有多个JVM进程,单应用锁无法跨JVM、跨进程。那么分布式锁的定义就出来了,分布式锁就是可以跨越多个JVM、跨越多个进程的锁,这种锁就叫做分布式锁。
目前存在的分布式锁的方案
通过第三方的组件实现跨JVM、跨进程的分布式锁。这就是分布式锁的解决思路,找到所有JVM可以共同访问的第三方组件,通过第三方组件实现分布式锁。

目前比较流行的分布式锁的解决方案有:
- 数据库,通过数据库可以实现分布式锁,但是在高并发的情况下对数据库压力较大,所以很少使用;
- Redis,借助Redis也可以实现分布式锁,而且Redis的Java客户端种类很多,使用的方法也不尽相同;
- Zookeeper,Zookeeper也可以实现分布式锁,同样Zookeeper也存在多个Java客户端,使用方法也不相同。
1、基于数据库悲观锁的分布式锁
通过for update语句实现锁:
select .... for update
for update是在数据库中上锁用的,可以为数据库中的行上一个排它锁。当一个事务的操作未完成时候,其他事务可以读取但是不能写入或更新。
2、基于Redis的Setnx实现分布式锁
获取锁的redis命令
SET resource_name my_random_value NX PX 30000
resource_name(key):资源名称,可根据不同的业务区分不同的锁。
my_random_value(value):随机值,每个线程的随机值都不相同,用于释放锁时的校验。
NX:表示 key不存在时设置成功,key存在则设置不成功。是一个原子性的操作,多线程并发时只有一个线程可以设置成功。设置成功才可以执行业务代码,不成功就不给执行。
PX:自动失效时间,即使出现异常情况,锁也可以通过过期来失效。
释放锁
释放锁时要校验之前设置的随机值(my_random_value),相同才能释放。因为需要校验,所以普通的redis的delete key命令不能满足,我们需要使用LUA脚本:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
代码实现
封装RedisLock
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.Expiration;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class RedisLock implements AutoCloseable {
private RedisTemplate redisTemplate;
private String key;
private String value;
//单位:秒
private int expireTime;
public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime=expireTime;
this.value = UUID.randomUUID().toString();
}
/**
* 获取分布式锁
* @return
*/
public boolean getLock(){
RedisCallback<Boolean> redisCallback = connection -> {
//设置NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//设置过期时间
Expiration expiration = Expiration.seconds(expireTime);
//序列化key
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
//序列化value
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
//执行setnx操作
Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
return result;
};
//获取分布式锁
Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
return lock;
}
public boolean unLock() {
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
List<String> keys = Arrays.asList(key);
Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
return result;
}
@Override
public void close() throws Exception {
unLock();
}
}
使用RedisLock
import com.distribute.lock.lock.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RedisLockController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("redisLock")
public String redisLock(){
log.info("我进入了方法!");
try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
if (redisLock.getLock()) {
log.info("我进入了锁!!");
Thread.sleep(15000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
log.info("方法执行完成");
return "方法执行完成";
}
}
3、基于Zookeeper的分布式锁
原理
利用了zookeeper的瞬时有序节点的特性,多线程并发创建顺势节点时,得到有序的序列,我们规定序号最小的线程获得锁。其他线程则监听自己序号的前一个序号,前一个线程执行完成,删除自己的序号的节点,下一个节点的线程得到通知,继续执行,以此类推。
代码实现
封装锁
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@Slf4j
public class ZkLock implements Watcher,AutoCloseable {
private ZooKeeper zooKeeper;
private String businessName;
private String znode;
public ZkLock(String connectString,String businessName) throws IOException {
this.zooKeeper = new ZooKeeper(connectString,30000,this);
this.businessName = businessName;
}
public boolean getLock() throws KeeperException, InterruptedException {
//创建业务 根节点
Stat existsNode = zooKeeper.exists("/" + businessName, false);
if (existsNode == null){
zooKeeper.create("/" + businessName,businessName.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
//创建瞬时有序节点 /order/order_00000001
znode = zooKeeper.create("/" + businessName + "/" + businessName + "_", businessName.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
znode = znode.substring(znode.lastIndexOf("/")+1);
//获取业务节点下 所有的子节点
List<String> childrenNodes = zooKeeper.getChildren("/" + businessName, false);
//子节点排序
Collections.sort(childrenNodes);
//获取序号最小的(第一个)子节点
String firstNode = childrenNodes.get(0);
//如果创建的节点是第一个子节点,则获得锁
//不是第一个子节点,则监听前一个节点
if (!firstNode.equals(znode)){
String lastNode = firstNode;
for (String node:childrenNodes){
if (!znode.equals(node)){
lastNode = node;
}else {
zooKeeper.exists("/"+businessName+"/"+lastNode,true);
break;
}
}
synchronized (this){
wait();
}
}
return true;
}
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getType() == Event.EventType.NodeDeleted){
synchronized (this){
notify();
}
}
}
@Override
public void close() throws Exception {
zooKeeper.delete("/"+businessName+"/"+znode,-1);
zooKeeper.close();
log.info("我释放了锁");
}
}
调用
import com.example.distributelock.lock.ZkLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class ZkLockController {
@RequestMapping("zkLock")
public String zkLock(){
log.info("我进入了方法!");
try (ZkLock zkLock = new ZkLock("localhost:2181","order")){
if (zkLock.getLock()) {
log.info("我进入了锁!!");
Thread.sleep(15000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
log.info("方法执行完成");
return "方法执行完成";
}
}
Redisson 实现分布式锁
Redisson是一个具有内存数据网格功能的Redis Java客户端,功能很多很强大,这里只介绍下它的分布式锁功能使用,在spring boot项目下。
我们日常开发中,推荐使用Redisson实现分布式锁。
https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
pom
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.2</version>
</dependency>
yml 连接redis
spring:
redis:
database: 0
host: 192.168.10.125
port: 6379
password: 123456
使用分布式锁
@Autowired
private RedissonClient redisson;
public void redissonLock() {
// 设置锁的业务分类
RLock rLock = redisson.getLock("xxx");
// 设置过期时间
rLock.lock(30, TimeUnit.SECONDS);
// 业务代码
rLock.unlock();
}

浙公网安备 33010602011771号