loviezhang 2020-04-23
日常的编码中,我们经常会遇到线程之间操作相同的资源导致并发
每一种开发技术或许都提供有代码级别的锁来避免这种并发问题,但是当服务器部署多个实例时,代码级别的锁是无法控制这样的并发的,这个时候我们遍可以通过Redis来控制功能对锁的获取,由于应用程序和Redis之间是通过网络来进行交流,无论是单机还是集群,无论是单线程还是多线程,利用Redis来实现锁都能够使用。
用Redis来创建锁,只要目标则是实现“占坑”,当前上下文我们将其称之为lock,假设一个线程已经占用了这个lock,那么其他的线程则无法再去占用这个lock,假设我们将占用的操作设置为set lock 1,那么则需要让其他的线程无法去set lock 1,常规的流程就是首先,获取到lock,如果lock没有被设置值,则设置进去,占用这个lock,但是同样的,这里在多线程和多实例的情况下会有并发问题,因为【获取到lock,如果lock没有被设置值,则设置进去,占用这个lock】这个操作并不是原子操作,可能在执行过程中,lock就被其他线程给占用设置,这种情况下,会由不止一个线程能够获取到锁。
为了解决这样的并发问题,在Redis中,提供了setnx指令来将【读取判断设置】的操作原子化,又由于Redis是一个单线程的执行模式,setnx将会只有一个线程能够设置值,通过这样的方法来实现锁的占用。而释放锁的方式则可以通过直接删除这个lcok的key或者设置过期时间,这里不是最主要的讨论目的
下面列举一个最简单的锁,控制几个线程中,只能由一个线程能够执行到逻辑代码,代码由JAVA编写,访问Redis利用RedisTemplate,在Redistemplate中setnx对应的方法为setIfAbsent,代码如下:
首先建立一个RedisLock的类,编写【读取判断设置】的操作以便于判断是否拿到锁:
@Component
public class RedisLock {
@Autowired
StringRedisTemplate redisTemplate;
public boolean isLock(String lockKey) {
Boolean result = redisTemplate.opsForValue().setIfAbsent("lock:" + lockKey, "1",
Duration.ofSeconds(5));
if (result) {
return false;
} else {
return true;
}
}
}建立一个MyThread来模拟多线程多实例并发占用某个Redis锁:
public class MyThread extends Thread {
int i;
private RedisLock redisLock;
public MyThread(int i, RedisLock redisLock) {
super();
this.i = i;
this.redisLock = redisLock;
}
@Override
public void run() {
final double d = Math.random();
final int sleep = (int)(d*100);
try { //随机睡眠
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (redisLock.isLock("test")) {
System.out.println("thread" + i + ": locked");
} else {
System.out.println("thread" + i + ": no lock");
}
}
}运行十个线程测试锁的获取情况
@Test
void contextLoads() throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread thread = new MyThread(i, redisLock);
thread.start();
}
Thread.sleep(1000);
}结果截图如下:

上述是一个加锁的简单实现,只有拿到锁的代码可以执行业务逻辑,同时也可以通过对Redis锁的状态监控实现类似代码中的lock等待
修改RedisLock代码如下:
@Component
public class RedisLock {
@Autowired
StringRedisTemplate redisTemplate;
public boolean isLock(String lockKey) {
Boolean result = redisTemplate.opsForValue().setIfAbsent("lock:" + lockKey, "1",
Duration.ofSeconds(5));
if (result) {
return false;
} else {
return true;
}
}
public void unLock(String lockKey) {
redisTemplate.delete("lock:" + lockKey);
}
}提供unlock方法以供释放锁
增加测试类Thread_Wait
public class MyThread_Wait extends Thread {
int i;
private RedisLock redisLock;
public MyThread_Wait(int i, RedisLock redisLock) {
super();
this.i = i;
this.redisLock = redisLock;
}
@Override
public void run() {
try {
final double d = Math.random();
final int sleep = (int) (d * 100);
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
int loopCount = 0;
String key = "test";
while (redisLock.isLock(key)) {
loopCount++;
Thread.sleep(10);
}
Thread.sleep(50);
System.out.println("thread" + i + " wait:" + loopCount * 10);
redisLock.unLock(key);
} catch (InterruptedException e) {
}
}
}开启十个线程等待锁的释放:
@Test
void contextLoads() throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread thread = new MyThread_Wait(i, redisLock);
thread.start();
}
Thread.sleep(3000);
}测试结果输出如下:

通过修改代码可以实现,对锁的监听超时时间或者重试获取锁的规则,上述演示了两种锁的简单实现
setIfAbsent