一、什么是分布式锁?
在多线程环境下,对于共享资源的访问,我们可以通过给线程加锁,来解决线程安全问题,保证数据的一致性。
在分布式架构中,应用程序是集群部署在不同的服务器上,这些应用程序进程之间是隔离的。对于共享资源数据的排他性访问,需要对共享资源加锁,而这个锁需要让所有的进程都访问到。此时,我们需要通过使用分布式锁解决共享资源的访问。使用分布式锁的主要原因是锁(互斥性)的使用范围发生了改变。
分布式锁的核心思想:首先获取锁、然后执行操作、最后释放锁。
分布式锁主要在多进程环境下,对共享资源的访问的场景下应用。比如:秒杀场景下,使用分布式锁防止超卖。
二、分布式锁的实现
分布式锁有如下几种实现方式:
- 基于数据库实现分布式锁。
- 基于Redis实现分布式锁。
- 基于zookeeper实现分布式锁
(1)基于数据库实现分布式锁
首先,在数据库中创建一张锁表,表中包含方法名等字段。并在方法名字段上创建唯一约束(唯一索引),通过使用方法名向表中插入数据,成功插入则获取到锁。获取到锁后,执行操作,操作完成后,删除数据释放锁。
实现步骤:
第一步:在数据库新建一张锁表(lock)。
DROP TABLE IF EXISTS `lock`;
CREATE TABLE `lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`desc` varchar(255) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
第二步:获取锁时向表中插入一条数据,由于有唯一约束,只会有一个线程插入成功,插入成功的线程获得锁,可以继续操作,没有插入成功的线程没有获得锁,不能操作。
INSERT INTO lock (method_name, desc) VALUES ('methodName', '测试的methodName');
第三步:解锁时,删除该条数据。
delete from lock where method_name ='methodName';
主要问题:
- 可用性差,数据库故障会导致业务系统不可用。
- 数据库性能存在瓶颈,不适合高并发场景。
- 没有失效机制,删除锁失败容易造成死锁。
- 锁的失效时间难以控制,需要通过定时任务去清理失效的key。
(2)基于Redis实现分布式锁
Redis里面提供了一些能够实现互斥特性的命令,比如SETNX (在key不存在的情况下为key设置值,key存在的话就不设置值),那么我们可以基于这些命令来去实现锁。
实现思想如下:
- 获取锁时,通过使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间自动释放锁, 锁的value1值为一个随机生成的uuid,用于在释放锁时进行判断。
- 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
- 释放锁的时候,通过uuid判断是不是该锁,若是该锁,则执行delete进行锁释放。
- 拿到锁期间,监控redis分布式锁是否需要延期,防止提前释放锁。
利用Redis实现分布式锁主要用到三个命令:
- setnx:setnx key value : 设置key及key的值,如果key存在返回设置失败,返回0;key不存在就设置成功,返回1;
- expire:expire key timeout: 设置key的过期时间。
- delete:delete key: 删除key
代码如下:
package com.lock.redis.jedis;
import com.lock.redis.jedis.util.JedisPoolInstance;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.UUID;
public class JedisDistributeLock {
private static final String redisLockPrefix = "redis:lock:";
/**
* 获取锁
*
* @param lockName
* @param acquireTimeOut 单位是毫秒 获取锁时间
* @param lockTimeOut 单位是毫秒 超时时间
* @return
*/
public String getRedisLock(String lockName, Long acquireTimeOut, Long lockTimeOut) {
String redisLockKey = redisLockPrefix + lockName;
String uniqueValue = UUID.randomUUID().toString();
//通过JedisPool创建一个jedis连接
JedisPool jedisPool = JedisPoolInstance.getJedisPoolInstance();
Jedis jedis = jedisPool.getResource();
try {
//超时时间 往后延acquireTimeOut秒(比如3秒)
Long endTime = System.currentTimeMillis() + acquireTimeOut;
//时间没有超过,有资格获取锁
while (System.currentTimeMillis() < endTime) {
//设置key 和 设置key的过期时间 不是原子操作(不在一个步骤中,是分了两步)
if (jedis.setnx(redisLockKey, uniqueValue) == 1) {
//设置key成功,表示拿到锁
jedis.pexpire(redisLockKey, lockTimeOut);
return uniqueValue;
} else {
if (jedis.ttl(redisLockKey) == -1) {
//设置过期时间
jedis.pexpire(redisLockKey, lockTimeOut);
}
}
//立刻马上去循环再获取锁,其实不是很好也没有什么意义,最好是稍等片刻再去重试获取锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
if (jedis != null) {
jedis.close();
}
}
return null;
}
/**
* 释放redis锁
*
* @param lockName
* @param uniqueValue
*/
public void releaseRedisLock(String lockName, String uniqueValue) {
//redis锁的key
String redisLockKey = redisLockPrefix + lockName;
Jedis jedis = JedisPoolInstance.getJedisPoolInstance().getResource();
try {
//自己的锁自己解,不要把别人的锁给解了
if (jedis.get(redisLockKey).equals(uniqueValue)) {
jedis.del(redisLockKey);
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
基于redisson客户端,实现Redis分布式锁方案如下:
redisson已经给我们提供现成的分布式锁,我们直接使用即可。
实现思想如下:
- 通过redissonClient 获取锁。
- 加锁,执行业务代码,提供了锁自动续期功能。
- 解锁,释放锁。
代码如下:
RLock rLock = redissonClient.getLock(lockName);
//加锁,然后下面的业务代码就会按顺序排队执行
rLock.lock(); //它有自动续期的
//执行业务代码
//......
//TODO 解锁,释放锁
if (rLock.isHeldByCurrentThread() && rLock.isLocked()) {
rLock.unlock();
}
(3)基于zookeeper实现分布式锁
zookeeper实现分布式锁采用其提供的临时有序节点+监听来实现。
实现思想如下:
- 创建一个锁的根结点 “/locks”,在构造方法初始化时,创建根节点。
- 获取锁时,现在根节点下创建一个临时顺序节点,然后获取根节点下所有子节点,判断当前节点是否为自小节点,如果是最小节点,则获取到锁,执行业务操作。如果不是最小节点,就监听前一个节点的删除事件,当前一个节点删除时,触发我的监听事件,获取到分布式锁。
- 释放锁:从zookeeper中删除当前节点。
基于zookeeper实现分布式锁有以下几种方式:
- Zookeeper原生客户端实现分布式锁。
- ZkClient第三方客户端实现分布式锁。
- Curator客户端给我们提供了现成的分布式互斥锁来实现分布式锁,所以我们不必自己开发。
使用Curator客户端实现分布式锁过程如下:
第一步:创建分布式互斥锁。
InterProcessMutex lock = new InterProcessMutex(zookeeperCuratorClient.client, “/storeLock”);
InterProcessMutex lock = new InterProcessMutex(zookeeperCuratorClient.client, "/storeLock");
第二步:获取分布式互斥锁。
if (lock.acquire(10, TimeUnit.SECONDS))
第三步:释放分布式互斥锁。
lock.release();