并发数据问题之分布式锁
架构不是凭空设计出来的,是长出来的
什么是分布式锁
分布式锁听起来很高大上,其实等你实际用的时候你会发现也还好,redis实现分布式锁的核心就是setnx指令;
场景示例
之前做过一个微信社群运营的项目,这个项目可以很好的诠释为啥我们会用分布式锁,大家有兴趣可以看看这个:
企业微信关于后台调用token说明:https://developer.work.weixin.qq.com/document/path/91039
使用分布式锁的前提:
- 分布式使用同一个标识调用
- 标识是变化的,且可能新标识获取会影响老标识的使用
架构示意
逻辑示意
这里重试设置锁也可以是直接获取失败丢弃,要看具体业务要求;
分布式锁redis实现代码示例
service
public String lockTest() {
String value = UUID.randomUUID().toString();
try {
boolean result = hadoopLock.tryGetLock(key, value, expireTime * 1000, blockTime * 1000);
if(!result){
return "获取锁失败,放弃执行";
}
System.out.println(Thread.currentThread().getName()+"获取锁后需要执行的代码");
try {
// 修眠模拟并发获取
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
// 释放锁
hadoopLock.releaseLockASC(key, value);
}
return Thread.currentThread().getName()+ "执行完毕";
}
hadoopLock-核心代码
/**
* 尝试获取锁
*
* @param key
* @param value
* @param expireTime 过期时间
* @param blockTime 阻塞时间 决定是尝试还是直接返回false
* @return 返回结果
*/
public boolean tryGetLock(String key, String value, long expireTime, long blockTime) {
try {
long startTryGetLockTime = System.currentTimeMillis();
return retryGetLock(key, value, expireTime, blockTime, startTryGetLockTime);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 重复获取锁
*
* @param key
* @param value
* @param expireTime
* @param blockTime
* @param startTryGetLockTime 开始获取锁的时间
* 这里可以暴力限流 获取不到锁不执行就好
* @return
*/
private boolean retryGetLock(String key, String value, long expireTime, long blockTime, long startTryGetLockTime) {
// 小于阻塞时间,可以一直重试获取
int count = 0; // 测试获取计数次数
while (System.currentTimeMillis() - startTryGetLockTime < blockTime) {
System.out.println(Thread.currentThread().getName() + "第" + count + "次" + "获取锁");
count++;
//如果不存在则插入
//Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
SetParams params = new SetParams().ex(10).nx();
String set = jedis.set(key, value, params);
if ("OK".equals(set)) {
System.out.println(Thread.currentThread().getName() + "获取锁成功");
// 获取锁成功 守护线程续期
// 守护线程开启
Thread watchDogThread = new Thread(new Runnable() {
@Override
public void run() {
int num = 0;
while (true){
num ++;
// 这里设置的过期时间是该value?续期1s
Boolean expire = redisTemplate.expire(key, 1000, TimeUnit.MICROSECONDS);
System.out.println("守护线程"+Thread.currentThread().getName()+"第"+num+"次续期,key="+key+",value="+value);
if(!expire){
return;
}
try {
Thread.sleep(900);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 设置为守护线程
watchDogThread.setDaemon(true);
watchDogThread.start();
return true;
}
try {
// 不成功继续获取
Thread.sleep(100);
} catch (InterruptedException e) {
return false;
}
}
return false;
}
public void releaseLockASC(String key, String value) {
String luaString = "local v = redis.call('get',KEYS[1]);if v then if v~=ARGV[1] then return 0;end;redis.call('del',KEYS[1]);end;return 1;";
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(value);
long eval = (long) jedis.eval(luaString, keys, args);
if (eval == 1L) {
System.out.println(Thread.currentThread().getName() + "释放锁 key=" + key + "," + "value = " + value);
}
}
项目地址:https://gitee.com/a-long-time-to-name-it/yzwselfproject.git