这一节中,我们将会使用上一节中分布式锁的原理:分布式锁的原理与实现 一、分布式锁的原理 。来一步一步的实现分布式锁。
一、实现的接口定义:
其中,我们先定义一个给外部使用的接口:
public interface DistributedLockService {
boolean tryLock(Runnable runnable, String key);
boolean tryLock(Runnable runnable, String key, int maxWaitTimeMills, int perWaitTimeMills);
}
可以看到,上述定义了两个方法,其中:
第一个方法是来根据key来执行runnable,如果拿到了分布式锁,那么将会执行runnable,并返回true,如果拿不到,不执行runnable,返回false。
第二个方法也是根据key来执行runnable,但是会有一个等待的时间,其中 maxWaitTimeMills 为最高的等待时间, perWaitTimeMills 为拿不到分布式锁的时候,多少毫秒来重试一次。
可能很多人对第二个方法有点疑问,是这样,当多个线程都在抢一个分布式锁的时候,比如A,B,C三个线程,同时时间只有一个能拿到,比如A线程,那么B,C线程拿不到,会等待 perWaitTimeMills 毫秒后,再去重试,知道从开始抢,到抢了n次之后,总时长超过了 maxWaitTimeMills 毫秒之后,会放弃,并返回false。
二、实现:
首先我们是使用Redis来进行实现的,所以本地必须启动一个Redis,在项目中,我们使用Jedis来连接操作Redis。
其中的原理请看上一篇的原理讲解:分布式锁的原理与实现 一、分布式锁的原理 。
此项目的全部源码已经发布到github上,地址为 项目源码地址 ,欢迎大家star和提问题。
具体的实现如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
/**
*
* Created by happyheng on 2018/4/6.
*/
@Service
public class DistributedLockServiceImpl implements DistributedLockService {
@Autowired
private Jedis jedis;
/**
* 分布式锁的超时时间,此设置为10s
*/
private static final int DISTRIBUTED_LOCK_TIME = 10;
/**
* 默认最大等待时间,此为5000ms
*/
private static final int DISTRIBUTED_DEFAULT_MAX_WAIT_TIME = 5000;
/**
* 默认单次等待时间,此为50ms
*/
private static final int DISTRIBUTED_DEFAULT_PER_WAIT_TIME = 50;
/**
* 分布式锁中对应value的随机位数
*/
private static final int DISTRIBUTED_KEY_SIGNATURE_LENGTH = 6;
/**
* 使用redis来获取分布式锁,返回的为当前设置的锁的签名,设置这个锁的签名是为了避免当前线程A拿到分布式锁后,阻塞时间过长,导致锁被超时删除
* ,被线程B拿到,线程A执行完成之后,可能又把线程B的删除掉,导致线程C又进入。
*
* @param key 对应key
* @return 但成功时返回的为当前设置的锁的签名, 失败时返回的为""空字符串
*/
private String lock(String key) {
// 得到一个随机的字符串
String keySignature = getRandomNumber();
long reply = jedis.setnx(key, keySignature);
if (reply > 0) {
// 设置过期时间
jedis.expire(key, DISTRIBUTED_LOCK_TIME);
return keySignature;
}
return "";
}
/**
* 删除分布式锁,注意此会传入keySignature,防止出现上述的误删情况
*
* @param key 分布式锁的key
* @param keySignature 分布式锁的value
*/
private void unLock(String key, String keySignature) {
String distributedLockValue = jedis.get(key);
// 当相等的时候才会删除
if (keySignature.equals(distributedLockValue)) {
jedis.del(key);
}
}
@Override
public boolean tryLock(Runnable runnable, String key) {
// 先获取分布式锁
String keySignature = lock(key);
if (StringUtils.isEmpty(keySignature)) return false;
// 获取到分布式锁,开始执行任务
runnable.run();
// 执行完成后,删除分布式锁
unLock(key, keySignature);
return true;
}
@Override
public boolean tryLock(Runnable runnable, String key, int maxWaitTimeMills, int perWaitTimeMills) {
maxWaitTimeMills = maxWaitTimeMills <= 0 ? DISTRIBUTED_DEFAULT_MAX_WAIT_TIME : maxWaitTimeMills;
perWaitTimeMills = perWaitTimeMills <= 0 || perWaitTimeMills >= maxWaitTimeMills ? DISTRIBUTED_DEFAULT_PER_WAIT_TIME : perWaitTimeMills;
long beginTimeMillis = System.currentTimeMillis();
// 尝试获取分布式锁,并执行任务
while (!tryLock(runnable, key)) {
// 当分布式锁失败时
// 超过最大时间时,不再循环
if (System.currentTimeMillis() - beginTimeMillis > maxWaitTimeMills) {
return false;
}
// 等待指定时间
try {
Thread.sleep(perWaitTimeMills);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
/**
* 得到分布式锁对应的value,解决阻塞或宕机造成的分布式锁失败的问题
*/
private String getRandomNumber() {
StringBuilder randomNumber = new StringBuilder();
for (int i = 0; i < DISTRIBUTED_KEY_SIGNATURE_LENGTH; i ++) {
randomNumber.append((int)(Math.random() * 10));
}
return randomNumber.toString();
}
}
其中 lock 方法即为 setnx 一个分布式锁,如果成功,此方法将会返回锁对应的随机字符串,之后即可使用这个随机字符串进行 unlock 。
三、测试
写好了分布式锁之后,如果进行测试呢?我们知道,一般在web项目中才会使用分布式锁,所以我们使用web项目来进行测试,而且,每一个请求,都是一个线程来执行,所以我写了如下的测试方法:
import com.happyheng.lock.DistributedLockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
*
* Created by happyheng on 2018/4/6.
*/
@RestController
@RequestMapping("/lock")
public class LockController {
@Autowired
private DistributedLockService distributedLockService;
@RequestMapping(value = "/test", method = RequestMethod.GET)
public String testDistributedLock(HttpServletRequest httpServletRequest) {
// 测试分布式锁
distributedLockService.tryLock(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + "启动");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + threadName + "结束");
}
}, "lock_test", 5000, 100);
return "";
}
}
可以看到,每次请求过来的时候,我都会执行一个runnable,里面会阻塞500ms,假如多个请求同时过来的话,同一时间也只有一个runnable会执行。
意思是如果分布式锁是成功的话,那么结果应该为
线程A启动
线程A结束
线程B启动
线程B结束
线程C启动
线程C结束
而不是
线程A启动
线程B启动
线程C启动
线程A结束
线程B结束
线程C结束
经测试,结果为
符合我们的预期。
四、结语:
我们最终实现了基于Redis的分布式锁,源码地址为 项目源码地址 ,在下一节中,我们将会使用注解来扩展分布式锁,使分布式锁能更好的融入到我们的项目中