简述
实现分布式锁首先要理解锁的使用场景,以及单机锁在分布式环境下会有什么问题,这里我做一些个人的理解如果有哪些错误的地方欢迎大家指正。
首先简述一下锁的使用场景:只有在多任务环境中多个任务都需要对同一共享资源进行写操作,并且对资源的访问是互斥的,简单来说就是当有多个线程同时操作同一个变量时,如果不加锁肯定会出现线程不安全的问题,就比如有100个商品,你设置5个线程去对这些商品的总额进行扣减,在不加锁的情况下你会发现扣减的数量与商品余量出现不一致甚至出现扣减数量与商品余量为负数的情况,这时候就需要用到锁了,java中我们常用的保持原子性操作的方式有synchronized、Lock、atomic当然这些都属于单机方式,它们解决不了分布式环境多任务对共享资源竞争的协同操作问题。
分布式锁方案,基于数据库的方案,利用redis的实现方案,利用zookeeper的实现方案,其中数据库方案主要是利用数据库自身提供的锁机制实现,要求数据库支持行级锁,这种方式性能差无法适应高并发场景并且容易出现死锁问题;redis主要基于setnx和lua脚本实现加锁和解锁;zookeeper方案主要是基于zk的节点特性以及watch机制实现。这里我们主要通过redis实现分布式锁。
环境准备
spring-boot 2.0、jedis、spring-data-redis
maven中需要引入的jar
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>2.1.15.RELEASE</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
这里在引用spring-data-redis时最好引用2.1.0以上的版本,因为在此版本中RedisTemplate的setIfAbsent()方法支持设置缓存的有效期。
内容
这里我实现分布式锁的方式主要是基于java的Lock接口实现。
1、创建redis操作的工具类RedisUtil.java
package com.union.study.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @description
* @date 2019/9/25 0025 23:08
*/
@Component
@Slf4j
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 指定缓存失效时间
* @param key 键
* @param time 有效期(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存存取
* @param key
* @return
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存存入
* @param key
* @param value
* @return
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* setnx存储
* @param lockKey
* @param value
* @param expireTime
* @return
*/
public boolean setnx(String lockKey, String value, Integer expireTime) {
return redisTemplate.opsForValue().setIfAbsent(lockKey, value,expireTime ,TimeUnit.MILLISECONDS);
}
/**
* 原子性删除
* @param key
* @return
*/
public String atomicityDel(String key, String uuid) {
/**lua脚本**/
String SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('publish', ARGV[1], ARGV[1]) return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(String.class);
redisScript.setScriptText(SCRIPT);
Object result = redisTemplate.execute(redisScript, Collections.singletonList(key), uuid);
return String.valueOf(result);
}
}
2、基于java的Lock实现redis的分布式锁RedisLock.java,这里我主要实现了阻塞锁lock()、尝试加锁tryLock()和释放锁unLock()三个方法,其中lock()基于CountDownLatch和redis的订阅发布机制实现了阻塞式加锁的方式,以及通过Future实现了加锁后对锁的心跳检测机制。
package com.union.study.lock;
import com.union.study.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @description
* @date 2020/2/23 15:10
*/
@Component
public class RedisLock implements Lock {
@Resource
private JedisConnectionFactory factory;
/**
* 分布式锁名称
*/
private static final String LOCK_KEY = "lock.key";
@Autowired
private RedisUtil redisUtil;
private static ThreadLocal<String> local = new ThreadLocal<>();
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
private static ConcurrentHashMap<String, Future> futures = new ConcurrentHashMap<>();
@Override
public void lock() {
if (tryLock()) {
return;
}
// 创建发令枪
CountDownLatch countDownLatch = new CountDownLatch(1);
// 创建消息订阅
Subscriber subscribe = new Subscriber(countDownLatch);
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
// 订阅redis上名称为local.get()的消息
jedis.subscribe(subscribe, local.get());
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
subscribe.unsubscribe();
lock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
String uuid=local.get();
if (uuid==null) {
uuid = UUID.randomUUID().toString();
local.set(uuid);
}
boolean isSuccess = redisUtil.setnx(LOCK_KEY, uuid, 30000);
if (isSuccess) {
// 加锁成功添加心跳检测
setHeartbeat(uuid);
return true;
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
redisUtil.atomicityDel(LOCK_KEY, local.get());
Future future = futures.get(local.get());
future.cancel(true);
futures.remove(local.get());
}
@Override
public Condition newCondition() {
return null;
}
/**
* 对分布式锁做心跳检测,为锁续命
* @param uuid
*/
private void setHeartbeat(String uuid) {
// 如果存在心跳检测直接返回
if (futures.contains(uuid)) {
return;
}
Future future = executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (redisUtil.get(LOCK_KEY).equals(uuid)) {
// 如果当前锁有效为锁续命
redisUtil.expire(LOCK_KEY, 30000);
} else {
// 当前锁无效,关闭当前心跳检测线程
futures.get(uuid).cancel(true);
futures.remove(uuid);
}
}
}, 1, 20, TimeUnit.SECONDS);
futures.put(uuid, future);
}
}
3、消息订阅类Subscriber.java
package com.union.study.lock;
import redis.clients.jedis.JedisPubSub;
import java.util.concurrent.CountDownLatch;
/**
* @description
* @date 2020/2/25 13:54
*/
public class Subscriber extends JedisPubSub {
private CountDownLatch cdl;
public Subscriber(CountDownLatch cdl) {
super();
this.cdl = cdl;
}
/**
* 取得订阅的消息后,通过发令枪再次尝试加锁
*/
@Override
public void onMessage(String channel, String message) {
cdl.countDown();
}
}
4、测试类
import com.union.study.BackAppMain;
import com.union.study.lock.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @description
* @date 2020/2/16 15:53
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = BackAppMain.class)
@Slf4j
public class ThreadTest {
public static Integer count = 100;
@Autowired
private RedisLock lock;
@Test
public void test () throws InterruptedException {
TicketRunnable runnable = new TicketRunnable();
Thread thread1 = new Thread(runnable, "1号窗口");
Thread thread2 = new Thread(runnable, "2号窗口");
Thread thread3 = new Thread(runnable, "3号窗口");
Thread thread4 = new Thread(runnable, "4号窗口");
Thread thread5 = new Thread(runnable, "5号窗口");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
Thread.currentThread().join();
}
public class TicketRunnable implements Runnable {
@Override
public void run() {
while (count > 0) {
lock.lock();
try {
if (count > 0) {
System.out.println("这是第"+Thread.currentThread().getName()+"售出的第" + count-- + "张票");
}
}catch (Exception e){
log.error("", e);
}finally {
log.info("-----------this is unlock----------");
lock.unlock();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
以上为我自己在本地基于redis实现的分布式锁的的方式,大致思路就是在redis通过setnx方式设置一个锁lock.key并对其设置失效时间防止死锁,当某个线程获得锁后直到其释放前其他线程都无法再次获取该锁,特别注意在释放锁时必须确认是当前锁的持有者才能进行此操作。