为什么使用分布式锁?
为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
什么是分布式锁?
分布式架构下解决共享资源访问安全问题
Redis解决分布式锁
- 加锁 setNX()上锁+expire锁过期
setnx 不存在才会设置成功,否则失败
expire key的过期时间
- 解锁 eval()+lua脚本
根据redis eval函数解析lua脚本的特性
-
实现
pom<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.0.1</version> </dependency> </dependencies>
yml
spring:
redis:
host: 127.0.0.1
port: 6379
password: root
RedisLock
@Component
public class RedisLock {
private String lock_key = "order_id_lock"; //锁键
private final long lock_internal_ime = 1000 * 3;//锁过期时间3秒
private final long timeout = 999999; //获取锁的超时时间
private final SetParams params = SetParams.setParams().nx().px(lock_internal_ime);//SET命令的参数
@Autowired
JedisPool jedisPool;
public boolean lock(String randomId) {
Jedis jedis = jedisPool.getResource();
jedis.auth("root");
Long start = System.currentTimeMillis();
try {
for (; ; ) {
//SET命令返回OK ,则证明获取锁成功
String res = jedis.set(lock_key, randomId, params);
if ("OK".equals(res)) {
return true;
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l >= timeout) {
return false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
jedis.close();
}
}
public boolean unlock(String randomId) {
Jedis jedis = jedisPool.getResource();
jedis.auth("root");
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList(lock_key),
Collections.singletonList(randomId));
if ("1".equals(result.toString())) {
return true;
}
return false;
} finally {
jedis.close();
}
}
}
App
@SpringBootApplication
@RestController
@Slf4j
public class App {
public static void main(String[] args) {
SpringApplication.run(App .class, args);
}
@Bean
public JedisPool jedisPool(){
return new JedisPool();
}
@Autowired
RedisLock redisLock;
//上锁计数器
int lockCount = 0;
//解锁锁计数器
int unlockCount = 0;
@RequestMapping("/index")
public String index() throws InterruptedException {
int clientcount = 100;
CountDownLatch countDownLatch = new CountDownLatch(clientcount);
ExecutorService executorService = Executors.newFixedThreadPool(clientcount);
long start = System.currentTimeMillis();
for (int i = 0; i < clientcount; i++) {
executorService.execute(() -> {
//雪花算法或UUID生成唯一ID
String id = UUID.randomUUID().toString();
try {
redisLock.lock(id);
lockCount++;
} finally {
redisLock.unlock(id);
unlockCount++;
}
countDownLatch.countDown();
});
}
countDownLatch.await();
long end = System.currentTimeMillis();
log.info("执行线程数:{},总耗时:{},上锁数:{},解锁数:{}", clientcount, end - start, lockCount,unlockCount);
return "Hello";
}
}
测试
1、启动本地redis
2、启动APP
3、http://127.0.0.1:8080/index
redisson实现分布式锁
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】
Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。---------来自百度百科
基于上述Redis解决分布式锁事例环境
添加如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.5</version>
</dependency>
yml修改成
spring:
redis:
host: 127.0.0.1
port: 6379
password: root
database: 0
timeout: 5000
配置
@Configuration
public class RedissonConfig {
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
/**
* RedissonClient,单机模式
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://" + host + ":" + port);
singleServerConfig.setTimeout(timeout);
singleServerConfig.setDatabase(database);
if (password != null && !"".equals(password)) { //有密码
singleServerConfig.setPassword(password);
}
return Redisson.create(config);
}
@Bean
public RedissonLocker redissonLocker(RedissonClient redissonClient) {
RedissonLocker locker = new RedissonLocker(redissonClient);
//设置LockUtil的锁处理对象
LockUtil.setLocker(locker);
return locker;
}
}
锁接口
/**
* 锁接口
* @author jie.zhao
*/
public interface Locker {
/**
* 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。
*
* @param lockKey
*/
void lock(String lockKey);
/**
* 释放锁
*
* @param lockKey
*/
void unlock(String lockKey);
/**
* 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。如果获取到锁后,执行结束后解锁或达到超时时间后会自动释放锁
*
* @param lockKey
* @param timeout
*/
void lock(String lockKey, int timeout);
/**
* 获取锁,如果锁不可用,则当前线程处于休眠状态,直到获得锁为止。如果获取到锁后,执行结束后解锁或达到超时时间后会自动释放锁
*
* @param lockKey
* @param unit
* @param timeout
*/
void lock(String lockKey, TimeUnit unit, int timeout);
/**
* 尝试获取锁,获取到立即返回true,未获取到立即返回false
*
* @param lockKey
* @return
*/
boolean tryLock(String lockKey);
/**
* 尝试获取锁,在等待时间内获取到锁则返回true,否则返回false,如果获取到锁,则要么执行完后程序释放锁,
* 要么在给定的超时时间leaseTime后释放锁
*
* @param lockKey
* @param waitTime
* @param leaseTime
* @param unit
* @return
*/
boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException;
/**
* 锁是否被任意一个线程锁持有
*
* @param lockKey
* @return
*/
boolean isLocked(String lockKey);
}
锁接口实现
/**
* 基于Redisson的分布式锁
* @author jie.zhao
*/
public class RedissonLocker implements Locker {
private RedissonClient redissonClient;
public RedissonLocker(RedissonClient redissonClient) {
super();
this.redissonClient = redissonClient;
}
@Override
public void lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void lock(String lockKey, int leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
}
@Override
public void lock(String lockKey, TimeUnit unit, int timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
}
public void setRedissonClient(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Override
public boolean tryLock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock();
}
@Override
public boolean tryLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit) throws InterruptedException {
RLock lock = redissonClient.getLock(lockKey);
return lock.tryLock(waitTime, leaseTime, unit);
}
@Override
public boolean isLocked(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
return lock.isLocked();
}
}
锁工具类
/**
* redis分布式锁工具类
* @author jie.zhao
*/
public final class LockUtil {
private static Locker locker;
/**
* 设置工具类使用的locker
*
* @param locker
*/
public static void setLocker(Locker locker) {
LockUtil.locker = locker;
}
/**
* 获取锁
*
* @param lockKey
*/
public static void lock(String lockKey) {
locker.lock(lockKey);
}
/**
* 释放锁
*
* @param lockKey
*/
public static void unlock(String lockKey) {
locker.unlock(lockKey);
}
/**
* 获取锁,超时释放
*
* @param lockKey
* @param timeout
*/
public static void lock(String lockKey, int timeout) {
locker.lock(lockKey, timeout);
}
/**
* 获取锁,超时释放,指定时间单位
*
* @param lockKey
* @param unit
* @param timeout
*/
public static void lock(String lockKey, TimeUnit unit, int timeout) {
locker.lock(lockKey, unit, timeout);
}
/**
* 尝试获取锁,获取到立即返回true,获取失败立即返回false
*
* @param lockKey
* @return
*/
public static boolean tryLock(String lockKey) {
return locker.tryLock(lockKey);
}
/**
* 尝试获取锁,在给定的waitTime时间内尝试,获取到返回true,获取失败返回false,获取到后再给定的leaseTime时间超时释放
*
* @param lockKey
* @param waitTime
* @param leaseTime
* @param unit
* @return
* @throws InterruptedException
*/
public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit) throws InterruptedException {
return locker.tryLock(lockKey, waitTime, leaseTime, unit);
}
/**
* 锁释放被任意一个线程持有
*
* @param lockKey
* @return
*/
public static boolean isLocked(String lockKey) {
return locker.isLocked(lockKey);
}
}
测试接口
static final String KEY = "LOCK_KEY";
@GetMapping("/test")
public Object test(){
//加锁
LockUtil.lock(KEY);
try {
//TODO 处理业务
System.out.println(" 处理业务。。。");
} catch (Exception e) {
//异常处理
}finally{
//释放锁
LockUtil.unlock(KEY);
}
return "SUCCESS";
}
测试
1、启动本地redis
2、启动APP
3、http://127.0.0.1:8080/test
zookeeper实现分布式锁
原理:持久节点+临时顺序节点+事件通知
1、所有临时顺序节点都在某个特定的持久节点下
1、每一个线程创建一个临时顺序节点
2、只有当为第一个临时顺序节点才能获取锁
3、每一个非临时顺序节点都会有上一个节点的监听
4、当上一个节点删除时判断自己是否为第一个临时顺序节点,是则获取锁
实现
pom
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
Lock
public interface Lock {
public void lock();
public void unLock();
}
AbstractLock
public abstract class AbstractLock implements Lock {
@Override
public void lock() {
if (tryLock()) {
System.out.println("#####" + Thread.currentThread().getName() + "获取锁成功######");
} else {
//一个拿到其他等待
waitLock();
}
}
@Override
public void unLock() {
System.out.println("#####\" + Thread.currentThread().getName() + \"释放锁成功######");
}
/**
* 获取锁失败进行等待
*/
abstract void waitLock();
/**
* 获取锁
*/
abstract boolean tryLock();
}
}
ZkDistributedLock
public class ZkDistributedLock extends AbstractLock {
private static final String CONNECTION = "127.0.0.1:2181";
private static final int TIMEOUT = 5000;
private ZkClient zkClient = new ZkClient(CONNECTION, TIMEOUT);
private static final String LOCK_PATH = "/lock_money";
private static final String REAL_PATH = LOCK_PATH + "/";
private String currentPath;
private String beforePath;
private CountDownLatch countDownLatch = null;
@Override
public void unLock() {
if (zkClient != null) {
//关闭连接会删除所有临时节点
zkClient.close();
}
}
@Override
boolean tryLock() {
if (!zkClient.exists(LOCK_PATH)) {
zkClient.createPersistent(LOCK_PATH);
}
if (StringUtils.isEmpty(currentPath)) {
//创建分布式锁【临时顺序节点】
currentPath = zkClient.createEphemeralSequential(REAL_PATH, "data");
//获取所有子节点
List<String> children = zkClient.getChildren(LOCK_PATH);
Collections.sort(children);
if (currentPath.equals(REAL_PATH + children.get(0))) {
//如果是第一个节点则获取锁,否则进行监听上一个节点
return true;
} else {
int i = Collections.binarySearch(children, currentPath.substring(REAL_PATH.length()));
//获取当前节点的上一个节点
beforePath = REAL_PATH + children.get(i - 1);
}
}
return false;
}
@Override
void waitLock() {
IZkDataListener listener = new IZkDataListener() {
// zk事件监听节点修改
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
//zk事件监听节点删除
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println("--------------节点删除" + s);
if (countDownLatch != null) {
// 计数器为0的情况,await 后面的继续执行,释放阻塞线程
countDownLatch.countDown();
}
}
};
// 监听上一个节点是否被删除,是就释放阻塞线程【轮到自己了】-》handleDataDeleted
// 只有上一个节点被删除当前节点才会继续执行
zkClient.subscribeDataChanges(beforePath, listener);
if (zkClient.exists(beforePath)) {
countDownLatch = new CountDownLatch(1);
try {
//阻塞其他线程,下面的代码不执行
countDownLatch.wait();
} catch (Exception e) {
// TODO
}
}
//此代码只要等beforePath节点删除后才会执行,既然删除了就没必要监听了
zkClient.unsubscribeDataChanges(beforePath, listener);
}
}
ZkTest
public class ZkTest implements Runnable {
/**
* 测试【先要启动zk】
*/
public static void main(String[] args) {
test();
}
private static int count;
private static void test() {
//多线程执行
//new ZkTest();放外面表示单jvm【单例】,里面表示多jvm【多例】
for (int i = 0; i < 10; i++) {
new Thread(new ZkTest()).start();
}
}
@Override
public void run() {
ZkDistributedLock lock = new ZkDistributedLock();
try {
lock.lock();
//运到zk连接超时一定要手动回滚事务
System.out.println("--------------订单号 -- " + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()) + " -- " + (++count));
} finally {
lock.unLock();
}
}
}
看到如下结果表示成功
基于zk扩展的curator实现分布式锁
-
Curator的分布式锁介绍
今天我们主要介绍这个基于Zookeeper实现的分布式锁方案(Curator),当然随着我们去了解Curator这个产品的时候,会惊喜的发现,它带给我们的不仅仅是分布式锁的实现。此处先不做介绍,我会另外用博客来记录,有兴趣的朋友可以自行下载这个项目来解读。 apache/curator
现在先让我们看看Curator的几种锁方案: -
四种锁方案
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器 -
实现
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.11.1</version> </dependency>
TestCurator
public class TestCurator {
private static Integer count = 1000000000;
/**
* 先要启动zk
*/
public static void main(String[] args) throws Exception {
//默认重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/myLock");
//多线程执行
//模拟多线程
for (int i = 0; i < 9; i++) {
Executors.newFixedThreadPool(10).execute(() -> {
try {
//获取锁
if (lock.acquire(5000, TimeUnit.MINUTES)) {
try {
//在分布式锁中进行业务操作线程是安全的
count = count / 10;
//测试输出递减数字说明成功
System.out.println("########" + Thread.currentThread().getName() + "########" + count);
} finally {
try {
//释放锁
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}