使用Redis实现了一个锁,命名为:RedisLock。
一、原理介绍
1.1 RedisLock特性
用最简单的方式实现一个redis分布式锁,该锁具有如下特性:
- 阻塞式:在加锁时,如果获取不到,则进入自旋状态;
- 非阻塞式:在加锁时,如果获取不到,立刻返回加锁失败;
- 可重入:同一个线程多次请求加锁,对资源做+1操作,解锁时,资源必须等于0才算完全解除锁定;
- 锁超时:加锁时,可以设置超时时间,如果在这个时间内没有获取到锁,则返回加锁失败;
- 锁过期:不允许用户设置过期时间,内部设置了一个默认60s的锁过期时间,防止锁长时间无法释放。
1.2 RedisLock对外提供的方法
- void lock():阻塞式锁,尝试获取锁、直到成功获取
- boolean tryLock():非阻塞式锁,只尝试一次,不论成功失败都返回
- boolean tryLock(int timeout):阻塞式锁,在超时时间内尝试获取
1.3 分布式锁使用注意事项
注意提高下分布式锁的优先级,当业务完成之后再释放。
举个例子,我有个业务,需要先查询数据库,如果没有该记录则创建一条,所以是两条sql:查询和插入,我为了保证分布式情况下只允许插入一条,就同时使用了事务和分布式锁。
我司中间件团队提供了基于注解的分布式锁,我在使用分布式注解时,同时给方法加上事务注解和分布式锁注解,结果偶然情况下会插入两条记录,排查分布式锁半天,发现锁没问题,在zk中创建了临时顺序节点,后来觉察出来可能是事务实行完还没提交,新的请求又进来了,所以把分布式锁提到外面去了。
分布式锁没起作用的代码:
public class XxxFacadeImpl{
public void methodA() {
methodB();
}
}
public class XxxServiceImpl {
@KLock(zk)
@Transactional
public void methodB() {
select(id);
if (没查到) {
insert(xxxPO);
}
}
}
修改后:
public class XxxFacadeImpl{
@KLock(zk)
public void methodA() {
methodB();
}
}
public class XxxServiceImpl {
@Transactional
public void methodB() {
select(id);
if (没查到) {
insert(xxxPO);
}
}
}
二、Redis实现分布式锁
package utils.lock;
import org.apache.commons.lang3.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.UUID;
public class RedisLock {
/**
* 每个线程持有自己的uuid,用于区分不同的锁
*/
private static final ThreadLocal<String> identifier = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());
private static final String LOCK_MSG = "OK";
private static final Long UNLOCK_MSG = 1L;
/**
* 锁过期时间默认值
*/
private static final int DEFAULT_EXPIRE_TIME = 60;
/**
* 阻塞式获取锁的间隔时间
*/
private static final long DEFAULT_SLEEP_TIME = 100;
// Redis连接池
private static String REDIS_IP = "192.168.160.128";
private static int REDIS_PORT = 6379;
private static JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), REDIS_IP, REDIS_PORT);
// 加锁的键
private String lockKey;
// 重入锁计数器
private int counter = 0;
public RedisLock(String lockKey) {
this.lockKey = lockKey;
}
/*******************************************
* 锁对外提供的使用方法
*******************************************/
/**
* 阻塞式锁,尝试获取锁、直到成功
* @throws InterruptedException
*/
public void lock() throws InterruptedException {
lock(lockKey, identifier.get());
}
/**
* 非阻塞式锁,只尝试一次,无论是否获取到锁都返回
* @return
* @throws InterruptedException
*/
public boolean tryLock() throws InterruptedException {
return tryLock(lockKey, identifier.get());
}
/**
* 阻塞式锁,在超时时间内尝试获取锁,默认100ms尝试一次
* @param timeout
* @return
* @throws InterruptedException
*/
public boolean tryLock(int timeout) throws InterruptedException {
return tryLock(lockKey, identifier.get(), timeout);
}
public boolean unlock() {
return unlock(lockKey, identifier.get());
}
public boolean isLocked() {
Jedis jedis = jedisPool.getResource();
String redisVal = jedis.get(lockKey);
return redisVal != null && redisVal.equals(identifier.get());
}
public void flushAll() {
Jedis jedis = jedisPool.getResource();
jedis.flushAll();
}
public void shutdown() {
jedisPool.destroy();
}
/*******************************************
* 以下是锁的内部实现
*******************************************/
private void lock(String key, String value) throws InterruptedException {
Jedis jedis = jedisPool.getResource();
while (true) {
if (setLockToRedis(key, value, jedis)) {
return;
}
}
}
private boolean tryLock(String key, String value, int timeout) throws InterruptedException {
Jedis jedis = jedisPool.getResource();
while (timeout >= 0) {
if (setLockToRedis(key, value, jedis)) {
return true;
}
timeout -= DEFAULT_SLEEP_TIME;
}
return false;
}
private boolean tryLock(String key, String value) throws InterruptedException {
Jedis jedis = jedisPool.getResource();
return setLockToRedis(key, value, jedis);
}
private boolean setLockToRedis(String key, String value, Jedis jedis) throws InterruptedException {
SetParams params = new SetParams();
params.nx().ex(DEFAULT_EXPIRE_TIME); // setNx,带超时时间
String result = jedis.set(key, value, params);
if (LOCK_MSG.equals(result)) {
jedis.close();
return true;
}
// 重入
String redisVal = jedis.get(key);
if (StringUtils.isNotBlank(redisVal) && redisVal.equals(value)) {
counter++;
jedis.close();
return true;
}
Thread.sleep(DEFAULT_SLEEP_TIME);
return false;
}
private boolean unlock(String key, String value) {
if (counter > 0) {
counter--;
return true;
}
Jedis jedis = jedisPool.getResource();
// 只有缓存的value和方法入参value相等时才删除缓存,保证只有加锁的线程有权限解锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
jedis.close();
return UNLOCK_MSG.equals(result);
}
}
三、测试
本地启动3个线程模拟分布式环境(条件不允许,凑合测吧)
3.1 阻塞式锁
package mytest.redis;
import utils.lock.RedisLock;
import java.util.concurrent.*;
/**
* @Description 分布式锁的测试
* @Author lilong
* @Date 2019-03-27 20:38
*/
public class RedisLockTest {
public static void main(String[] args) {
RedisLock redisLock = new RedisLock("testLock");
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i < 1; i++) {
threadPool.execute(() -> {
try {
testBlockedLock(redisLock);
System.out.println("######### " + Thread.currentThread().getId() + " 馍馍片");
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Game Over!");
threadPool.shutdown();
redisLock.flushAll(); // 清空缓存
}
/**
* 阻塞式锁
*
* @param redisLock
* @throws InterruptedException
*/
private static void testBlockedLock(RedisLock redisLock) throws InterruptedException {
System.out.println("######### " + Thread.currentThread().getId() + " 加锁中...");
redisLock.lock();
boolean isLocked = redisLock.isLocked();
try {
System.out.println("######### " + Thread.currentThread().getId() + (isLocked ? " 加锁成功!" : " 加锁失败!"));
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (redisLock.unlock()) {
System.out.println("######### " + Thread.currentThread().getId() + " 解锁成功");
} else {
System.out.println("######### " + Thread.currentThread().getId() + " 解锁失败");
}
}
}
}
打印:
符合happens-before语义中的:解锁先于加锁。
3.2 非阻塞式锁
复用上面的main方法,更改测试锁的方法:
/**
* 非阻塞式锁
*
* @param redisLock
* @throws InterruptedException
*/
private static void testNonBlockingLock(RedisLock redisLock) throws InterruptedException {
System.out.println("######### " + Thread.currentThread().getId() + " 加锁中...");
boolean isLocked = redisLock.tryLock();
try {
System.out.println("######### " + Thread.currentThread().getId() + (isLocked ? " 加锁成功!" : " 加锁失败!"));
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (redisLock.unlock()) {
System.out.println("######### " + Thread.currentThread().getId() + " 解锁成功");
} else {
System.out.println("######### " + Thread.currentThread().getId() + " 解锁失败");
}
}
}
打印:
只有线程16加锁成功,其他线程都加锁失败。
3.3 超时锁
timeout设置为1s,而线程睡眠3s,因此只有一个线程能拿到锁,其它线程都会在等待获取锁的过程中超时。
/**
* 带超时时间的阻塞式锁
*
* @param redisLock
* @throws InterruptedException
*/
private static void testBlockedLockWithTimeout(RedisLock redisLock) throws InterruptedException {
System.out.println("######### " + Thread.currentThread().getId() + " 加锁中...");
boolean isLocked = redisLock.tryLock(1000);
try {
System.out.println("######### " + Thread.currentThread().getId() + (isLocked ? " 加锁成功!" : " 加锁失败!"));
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (redisLock.unlock()) {
System.out.println("######### " + Thread.currentThread().getId() + " 解锁成功");
} else {
System.out.println("######### " + Thread.currentThread().getId() + " 解锁失败");
}
}
}
打印:
只有线程15加锁成功,其他线程加锁失败。
3.4 重入锁
/**
* 重入锁
*
* @param redisLock
* @throws InterruptedException
*/
private static void testReentrantLock(RedisLock redisLock) throws InterruptedException {
int i = 1;
try {
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次加锁中...");
redisLock.lock();
boolean isLocked = redisLock.isLocked();
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" +(isLocked ? "加锁成功!" : "加锁失败!"));
Thread.sleep(1000);
i++;
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次加锁中...");
redisLock.lock();
isLocked = redisLock.isLocked();
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" +(isLocked ? "加锁成功!" : "加锁失败!"));
Thread.sleep(1000);
i++;
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次加锁中...");
redisLock.lock();
isLocked = redisLock.isLocked();
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" +(isLocked ? "加锁成功!" : "加锁失败!"));
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
i = 1;
if (redisLock.unlock()) {
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁成功");
} else {
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁失败");
}
i++;
if (redisLock.unlock()) {
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁成功");
} else {
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁失败");
}
i++;
if (redisLock.unlock()) {
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁成功");
} else {
System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁失败");
}
}
}
打印:
可以看到,对于同一把锁,多个线程可以多次获取到锁(可重入),当然了,加多少次锁就要释放多少次锁,这样才能完全释放掉锁,别的线程才能继续获取。
4 其他
4.1 Redis安装
1)去redis官网(https://redis.io/)下载最新版本,我用的redis-5.0.4.tar.gz,下载完拷贝到虚拟机里边
2)解压文件:tar xzvfp redis-5.0.4.tar.gz,会生成一个文件夹:redis-5.0.4
3)cd redis-5.0.4 ,进入文件夹,输入make命令进行编译,如果编译不通过, 可能会报缺少编译环境c、cpp啥的,需要先解决掉这些问题;
4)编译完成之后,进入 src文件夹,运行./redis-server,启动redis服务端;
5)如果客户端提示权限失败啥的,可能还要在运行时加上配置文件:
[root@localhost redis-5.0.4]# ./src/redis-server redis.conf
4.2 查看Redis中的保存的锁
1)运行redis客户端
[root@localhost src]# ./redis-cli
2)查看锁记录
我们刚才的案例中保存的锁的名称叫“testLock”,我们可以用“get testLock”命令查看保存的内容。
为了看到加锁、解锁过程中redis记录的变化,我们启用单个线程,并且在加锁后、解锁后打断点:
- 未加锁:
- 加锁时:
- 解锁后: