题外话
小弟第一次发表博客,水平有限,如有错误,请及时指出。
简介
本文是在redis单机前提下,保证分布式锁不会出现BUG;如果redis是集群,那么在极端情况下会出现锁失效;
- 设计思路
根据redis中的setnx命令,可实现最简单的锁。setnx的返回值(0或1)可理解为是否加锁成功。 - setnx引起的问题
1.假如定义锁的key为lock,那么当线程1获得锁后,该什么时候去删除锁?可在finally中删除锁。
2.如果线程1出现某些异常,导致锁没有被删除,出现了死锁,该如何解决?设置锁的过期时间。
3.如果锁的过期时间小于业务代码的执行时间,也就是说,代码还没执行完,锁就已经失效了,如何解决?可在获取锁后,启动一个线程去定时更新锁的过期时间。
代码
测试环境是springboot+redis。
注意:如果没有这个方法,请升级spring-data-redis的版本stringRedisTemplate.opsForValue().setIfAbsent(lockKey, id, 2, TimeUnit.SECONDS);
获取锁的代码,代码中包含了验证锁是否有效
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁
*/
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//这个map的作用是:防止stringRedisTemplate.delete(lockKey);删除失败,导致更新时间的那个线程会一直存在,一直更新失效时间,从而导致死锁
public volatile static Map<String, String> lockMap = new HashMap<>(1);
public void test1(int i) {
String lockKey = "lockKey";
/*
生成id,防止在高并发情况下被其他线程删除锁;其实这种误删除的情况只会存在键过期的情况下,
但是锁的过期时间其实是会一直更新的,所以这里是防止锁失效,防范于未然。
上面举例:假如在不更新过期时间的情况下,方法执行需要15s,但是键只存在10s,那么线程1在10s后就会释放锁,
线程2加上锁以后,线程2执行5s时,线程1的finally中就会去释放锁,这样就会导致后面一系列的锁失效
*/
String id = UUID.randomUUID().toString();
try {
boolean flag = false;
int flagNum = 0;
//循环拿锁,并且最多拿10次
while (!flag/* && flagNum < 10*/) {
flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, id, 3, TimeUnit.SECONDS);
flagNum++;
}
//加锁成功
if (flag) {
//加锁成功后,在jvm中放一个加锁标记
lockMap.put(id, id);
//起一个线程去更新这个锁的过期时间
ExecutorService threadPool = MyThreadPoolExecutor2.getThreadPool();
threadPool.submit(new Runnable() {
@Override
public void run() {
boolean idFlag = true;
while (idFlag) {
try {
//定时检查锁是还是否存在;检查时间=过期时间的1/3
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
//判断该线程是否还持有这把锁
//这里是个bug,判断是否持有锁和刷新锁时间,不是原子性的。应该用lua脚本写一起
idFlag = id.equals(stringRedisTemplate.opsForValue().get(lockKey)) && lockMap.get(id) != null;
if (idFlag) {
//更新锁的过期时间
stringRedisTemplate.expire(lockKey, 3, TimeUnit.SECONDS);
System.out.println("我更新了时间::" + i);
System.out.println("过期时间为::" + stringRedisTemplate.getExpire(lockKey));
}
}
}
});
System.out.println("线程::" + i + "::拿到锁了" + Thread.currentThread().getName());
//这里写自己的业务 以下为测试数据
String test = "test";
String sum = stringRedisTemplate.opsForValue().get(test);
stringRedisTemplate.opsForValue().set(test, String.valueOf(Integer.valueOf(sum) + 1));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lockMap.remove(id);
//这里是个BUG,判断锁是否属于当前线程后,再去释放锁,两个步骤不是原子性的;
//如果判断当前锁是属于当前线程的,但是刚判断完,当前线程的锁就自动释放了,然后再去执行delete,那是不是把别的线程的锁释放了呢
if (id.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
System.out.println("删除的id::" + id);
stringRedisTemplate.delete(lockKey);
}
}
}
}
测试入口
@RestController
public class RedisLockController {
@Autowired
private RedisLock redisLock;
@RequestMapping("/test")
public String test() {
ExecutorService threadPool = MyThreadPoolExecutor.getThreadPool();
for (int i = 0; i < 100; i++) {
threadPool.submit(new Run(i,redisLock));
}
return "抢锁";
}
}
相关类
import com.redis.utils.RedisLock;
/**
* Created by Administrator on 2020/9/11.
*/
public class Run implements Runnable{
private int i;
private RedisLock redisLock;
public Run(int i, RedisLock redisLock) {
this.i = i;
this.redisLock = redisLock;
}
@Override
public void run() {
/*try {
Thread.sleep(2800);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
redisLock.test1(i);
}
}
Redlock
该代码还未解决redis集群下产生的问题,如果redis主节点刚加好锁,在这时,主节点挂了,别的线程在从节点上也能获取到锁,因为主节点在挂的时候,加锁信息还未同步到从节点上。
解决方案:可用redlock。参考文档和视频:
https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_1003days.html
https://www.bilibili.com/video/BV1Vt4y1D7BH?p=5
总结一下:实现起来很复杂,如果真的需要做redis集群的分布式锁,还是用zk吧。
redisson实现分布式锁
redis分布式锁也可使用redisson,这是别人封装好的。
Redisson好像也没有解决主从集群情况下,主挂了出现的问题。
以下为单机redis加锁的测试代码
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.4</version>
</dependency>
@Component
public class RedisLock {
@Autowired
private Redisson redisson;
public void redisson(int i) {
String lockKey = "lockKey";
RLock lock = redisson.getLock(lockKey);
try {
//lock这里会阻塞,直到抢到锁为止。锁的默认过期时间为30s
lock.lock();
System.out.println("线程::" + i + "::拿到锁了" + Thread.currentThread().getName());
//这里写自己的业务 以下为测试数据
String test = "test";
String sum = stringRedisTemplate.opsForValue().get(test);
stringRedisTemplate.opsForValue().set(test, String.valueOf(Integer.valueOf(sum) + 1));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
@RestController
public class RedisLockController {
@Autowired
private RedisLock redisLock;
@RequestMapping("/test")
public String test() {
ExecutorService threadPool = MyThreadPoolExecutor.getThreadPool();
for (int i = 0; i < 100; i++) {
threadPool.submit(new Run(i,redisLock));
}
return "抢锁";
}
}
public class Run implements Runnable{
private int i;
private RedisLock redisLock;
public Run(int i, RedisLock redisLock) {
this.i = i;
this.redisLock = redisLock;
}
@Override
public void run() {
/*try {
Thread.sleep(2800);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
redisLock.redisson(i);
}
}
总结
redis锁虽然迸发高,但是使用不如zk的分布式锁,接下来会研究zk的分布式锁。