在多线程并发的情况下,我们可以使用锁来保证代码在同一时间只能一个线程访问,比如synchronize或者lock。但在分布式的集群环境,就需要使用分布式锁。
分布式:一个业务拆分为多个子业务,部署在多个服务器上 。
集群:同一个业务,部署在多个服务器上 。
a).避免不同节点(业务)重复相同工作,
b).避免破坏数据的正确性
比如,同一服务部署在不同的服务器上,同时接受到请求,同时对数据进行修改(如i- -),则可能出现重复- -的情况。
Set命令附加NX和PX属性
主要依托了redis的 Key 不存在时才能 Set 成功的特性
A服务set成功(获取锁成功),那么B服务就无法获取改Key的锁,除非A服务释放了锁,B服务才能获取到。,
a.锁超时:
A服务set成功(获取了锁),之后A服务突然挂掉了,那么其它服务就无法获取到锁了,为了避免这种问题,所以需要添加锁过期时间,像上述图片t2在30s后会自动失效,即释放锁。
b.原子性问题:
加锁的时候,如果先set t2 666,然后再expire t2 30 ;这样的操作不是原子性的,也就是说,如果set t2 666后突然服务挂了,又还没来得及设置过期时间,则其他服务永远也获取不到锁,但是set t2 666 PX 30000 NX这样就是原子性的。
解锁的时候,如果直接jedis.del(key);可能会删除不是自己加的锁。,
是否会想到下述的操作,先get判断再del,但这并不是原子性的操作,比如A服务加锁后,到了过期时间自动释放了锁,之后B服务获取了锁,此时A服务业务逻辑执行完后,直接就把锁给删了,服务就乱套了。
if (value.equals(jedis.get(key))) {
// 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁
jedis.del(key);
}
正确释放锁,需要执行lua脚本来进行。
c.业务执行超过自动过期时间:
A服务获得了锁,业务执行时间超过了过期时间,A服务还未手动释放锁就自动释放了锁,然后B服务获得了锁,也有可能出现别的问题,所以为了避免这个问题, Redis 分布式锁不要用于较长时间的任务。
或者对获得锁的线程启动一个守护线程,用来给锁续期(执行expire命令),执行时间就变长了这样。
d.请求是否不可丢失问题:
如果请求不允许丢失,即每个服务都要获取锁来执行业务逻辑,则不断重试获取锁,直到获取锁成功。
如果允许丢失,则尝试重试到设置的重试时间阈值,无法获取锁则不管了,也就不执行业务逻辑了。
加锁与释放锁的类:
package com.zwh.JedisDistributed;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
public class RedisLock {
public String LOCK_KEY = "reids_lock";
private long AUTO_EXPIRE_TIME; //锁自动释放阈值
private long reTryTime; //重试时间阈值
private SetParams params ;
private static GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
public static JedisPool jedisPool = null;
public RedisLock(Integer inventory, long AUTO_EXPIRE_TIME,long reTryTime) {
this.AUTO_EXPIRE_TIME = AUTO_EXPIRE_TIME;
this.params = SetParams.setParams().nx().px(AUTO_EXPIRE_TIME);
this.reTryTime=reTryTime;
poolConfig.setMaxIdle(inventory);
poolConfig.setMaxTotal(inventory);
poolConfig.setMaxWaitMillis(2000);//2s后报错
jedisPool = new JedisPool(poolConfig, "192.168.199.188", 6379,4000);//4s后报连接超时
}
/**
* 尝试获取锁,到了重试时间阈值退出,即可能请求丢失
* */
public String lock(String id) {
Long start = System.currentTimeMillis();
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
for (; ; ) {
//SET命令返回OK ,则证明获取锁成功
String lock = jedis.set(LOCK_KEY, id, params);
if ("OK".equals(lock)) {
return "OK";
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l >= reTryTime) {
return "TimeOut";
}
Thread.sleep(200);
}
} catch (Exception e) {
System.out.println("******异常*******:"+e.toString());
} finally {
try {
if (jedis != null) {
// System.out.println(jedisPool.getNumWaiters()+"----链接活跃数:"+jedisPool.getNumActive()+"----空闲连接数:"+jedisPool.getNumIdle());
jedis.close();
// System.out.println(jedisPool.getNumWaiters()+"----Close后活跃数:"+jedisPool.getNumActive()+"----空闲连接数:"+jedisPool.getNumIdle());
}
} catch (Exception e) {
e.printStackTrace();
}
}
return "";
}
/**
* 释放锁方法
* */
public boolean unlock(String id) {
Jedis jedis = null;
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
jedis = jedisPool.getResource();
String result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();
return "1".equals(result) ? true : false;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
}
测试类:
import com.zwh.JedisDistributed.RedisLock;
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test {
private static Integer POOL_SIZE=1001;
private static long AUTO_EXPIRE_TIME = 3000;
private static int NUM =1000;
private static long reTryTime = 50000;
private static LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue();
static RedisLock redisLock = new RedisLock(POOL_SIZE,AUTO_EXPIRE_TIME,reTryTime);
public static void getDeamon() {
Thread deamonThread = new Thread(new Runnable() {
@Override
public void run() {
try (Jedis jedis = redisLock.jedisPool.getResource()) {
while (true){
Thread.sleep(2000);
System.out.println("ttl:"+jedis.ttl(redisLock.LOCK_KEY));
jedis.expire(redisLock.LOCK_KEY, 3);
// System.out.println("----------续期了----------"+Thread.currentThread().getName());
}
} catch (Exception e) {
System.out.println("守护线程异常:" + e.toString());
}
}
});
deamonThread.setDaemon(true);
deamonThread.start();
}
/**
* 业务处理超过自动过期时间,守护线程为获取锁续期。
* */
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
for (int i = 0; i <50; i++) {
final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
String uuid =UUID.randomUUID().toString();
String lockResult = redisLock.lock(uuid); //重试获取锁失败时,请求丢失
if ("OK".equals(lockResult)){
getDeamon(); //启动守护线程,每2s续期3s
try {
Thread.sleep(2000); //业务逻辑处理耗时
NUM--;
System.out.println("-------库存NUM:"+NUM);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean unlock = redisLock.unlock(uuid);
// System.out.println("************unlock:"+unlock+"******"+Thread.currentThread().getName());
}else {
System.out.println("重试超时:"+lockResult);
}
}
});
}
executor.shutdown();
}
/**
* 业务处理超过自动过期时间,不做处理
* */
public static void main1(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
for (int i = 0; i <100; i++) {
// final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
String uuid =UUID.randomUUID().toString();
String lockResult = redisLock.lock(uuid);//重试获取锁失败时,请求丢失
if ("OK".equals(lockResult)){
long start = System.currentTimeMillis();
try {
Thread.sleep(100); //业务逻辑处理耗时
NUM--;
System.out.println("-------库存NUM:"+NUM);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
if ((end-start)<=AUTO_EXPIRE_TIME){
boolean unlock = redisLock.unlock(uuid); //成功获得了锁并且业务处理为超时
}else{
System.out.println("------------------业务超时---------------------");
}
}else {
System.out.println("重试超时:"+lockResult);
}
}
});
}
executor.shutdown();
}
}
参考资料:
漫画:什么是分布式锁?
Redis—分布式锁深入探究
姗姗来迟的Redis分布式锁
代码地址:
https://github.com/OooooOz/Redis
//-------------------------------------------------------相互交流--------------------------------------------------//