分布式系统核心要求:分布式锁实现之REDIS版本

项目中使用到了redis如果这块没有严格要求,可以直接采用REDIS分布式锁。 在单体应用中直接通过线程notifyAll()实现线程之间协作,分布式中需要通过事件通知或消息发布订阅来实现。

目录

缺点与参考实现

Redis分布式锁核心命令

分布式锁使用粗放版本

SpringBoot RedisTemplate版本

Redis发布订阅解决分布式消息问题

频道和消息枚举

消息发布订阅消息监听类

消息发布线程

消息订阅线程

分布式锁初始化和消息发布实现

使用Arthas监控跟踪

watch

trace

tt


缺点与参考实现

优点:性能高,实现方便,在允许偶发的锁失效情况下,不影响系统正常使用建议使用缓存方式的锁。

缺点:通过锁超时机制不是十分可靠,当线程获得锁后,因处理时间很长导致锁超时,锁即失效。

参考实现: 

Java中wait、notify、notifyAll使用详解

分布式锁之Redis实现

Jedis分布式锁实现

Redis 发布订阅

Redis之发布订阅(Java)

Redis发布订阅模式

Redis分布式锁核心命令

SET resource_name my_random_value NX PX 30000

分布式锁使用粗放版本

Java代码如下:

@Component
public class RedisDistributedLock {

	private static Object lock = new Object();

	private static final String LOCK_SUCCESS = "OK";

	private static final String SET_IF_NOT_EXIST = "NX";

	private static final String SET_WITH_EXPIRE_TIME = "PX";

	private static final Long RELEASE_SUCCESS = 1L;

	/**
	 * 尝试获取分布式锁[记得关闭连接]
	 * 
	 * @param lockKey
	 *            锁
	 * @param requestId
	 *            请求标识
	 * @param expireTime
	 *            超期时间(ms)
	 * @return 是否获取成功
	 */
	public boolean tryGetDistributedLock(String lockKey, String requestId,int expireTime) {
		Jedis jedis = null;
		try {
			if (XHTSystemConfig.clusterModeForTomcat&&XHTSystemConfig.clusterRedisEnableWrite) {
				jedis = RedisNodeManagerUtil
						.getJedisFromPool(Constants.CLUSTER_ENABLE_WRITE_NAME);
			} else {
				jedis = RedisPool.getJedis();
			}
			// 获得锁
			synchronized(lock){
				String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST,	SET_WITH_EXPIRE_TIME, expireTime);
				if (LOCK_SUCCESS.equals(result)) {
					return true;
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (null != jedis) {
				jedis.close();
			}
		}
		return false;
	}

	/**
	 * 释放分布式锁[记得关闭连接]
	 * 
	 * @param lockKey
	 *            锁
	 * @param requestId
	 *            请求标识
	 * @return 是否释放成功
	 */
	public boolean releaseDistributedLock(String lockKey, String requestId) {
		Jedis jedis = null;
		try {
			if (XHTSystemConfig.clusterModeForTomcat&&XHTSystemConfig.clusterRedisEnableWrite) {
				jedis = RedisNodeManagerUtil
						.getJedisFromPool(Constants.CLUSTER_ENABLE_WRITE_NAME);
			} else {
				jedis = RedisPool.getJedis();
			}
			// 释放锁
			synchronized(lock){
				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(lockKey),
						Collections.singletonList(requestId));
				if (RELEASE_SUCCESS.equals(result)) {
					lock.notifyAll();
					return true;
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (null != jedis) {
				jedis.close();
			}
		}
		return false;
	}

	/**
	 * 分布式阻塞获取锁
	 * 
	 * @param key
	 */
	public String tryGetDistributedLockByBlocking(String lockKey, String requestId, int seconds) {
		boolean success = tryGetDistributedLock(lockKey, requestId, seconds*1000);
		if (success) {
			return requestId;// 获取成功直接返回
		}
		long waiting = seconds*1000;
		long end = System.currentTimeMillis() + waiting;
		while (!success && waiting >= 0) {
			// 同一个应用接收到通知会释放掉锁--分布式下其他节点暂时没实现通知(ZooKeeper可以实现事件监听,暂不引入)
			synchronized(lock){
				try {
					lock.wait(waiting);//被通知时释放锁解除等待
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			// 尝试获取锁
			success = tryGetDistributedLock(lockKey, requestId, seconds);
			waiting = end - System.currentTimeMillis();
		}
		if (success) {
			return requestId;
		} else {
			return Constants.CLUSTER_LOCK_FAILED;
		}
	}

}

SpringBoot RedisTemplate版本

try{
	lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK);
	logger.info("cancelCouponCode是否获取到锁:"+lock);
	if (lock) {
		// TODO
		redisTemplate.expire(lockKey,1, TimeUnit.MINUTES); //成功设置过期时间
		return res;
	}else {
		logger.info("cancelCouponCode没有获取到锁,不执行任务!");
	}
}finally{
	if(lock){	
		redisTemplate.delete(lockKey);
		logger.info("cancelCouponCode任务结束,释放锁!");		
	}else{
		logger.info("cancelCouponCode没有获取到锁,无需释放锁!");
	}
}

Redis发布订阅解决分布式消息问题

分布式消息通知简单实现即可,集群每个节点都订阅相同的频道,消息发布都能接收到消息。因为单服务的线程通知对集群里面的其他服务是隔离的,所以这里采用Redis的消息发布订阅模式来实现事件的通知。

频道和消息枚举

public enum RedisChannel {

	/**
	 * channel:分布式锁通道
	 */
	DISTRIBUTE_LOCK("DISTRIBUTE_LOCK"),
	/**
	 * value:通知所有消息类型
	 */
	NOTIFYALL("NOTIFYALL");
	
	private String val="";
	
	private RedisChannel(String val){
		this.val = val;
	}

	@Override
	public String toString() {
		return val;
	}

}

消息发布订阅消息监听类

public class RedisPubSub extends JedisPubSub{
	
	private static Logger logger = Logger.getLogger(RedisPubSub.class);
	

	/**
	 * 消息处理(其他没有用的方法可以不重载)
	 */
	@Override
	public void onMessage(String channel, String message) {
		// TODO Auto-generated method stub
		super.onMessage(channel, message);
		// 处理频道消息
		switch(channel){
			// 分布式锁
			case "DISTRIBUTE_LOCK":
				if(message.equals(RedisChannel.NOTIFYALL)){
					synchronized(RedisDistributedLock.lock){
						//通知当前应用wait处理
						RedisDistributedLock.lock.notifyAll();
						logger.error(String.format("REDIS 消息:DISTRIBUTE_LOCK  NOTIFYALL={%s} ", RedisChannel.NOTIFYALL));
					}
				}
			    break;
			default:
				logger.error(String.format("REDIS 消息:channel= {%s}  message= {%s}", channel,message));
				break;
		}

	}

}

消息发布线程

public class Publisher extends Thread{
	
	private Jedis jedis;
	private String channel;
	private String message;
	
	public Publisher(Jedis jedis,String channel,String message){
		this.jedis = jedis;
		this.channel = channel;
		this.message = message;
	}

	@Override
	public void run() {
		try {
			//发布特定消息
			jedis.publish(channel, message);
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
			jedis.close();
		}
	}

}

消息订阅线程

public class Subscriber extends Thread{
	
	private Jedis jedis;
	private JedisPubSub jedisPubSub;
	private String channel;
	
	public Subscriber(Jedis jedis,RedisPubSub jedisPubSub,String channel){
		this.jedis = jedis;
		this.jedisPubSub = jedisPubSub;
		this.channel = channel;
	}

	@Override
	public void run() {
		try {
			// 订阅特定消息
			jedis.subscribe(jedisPubSub, channel);
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			jedis.close();
		}
	}
	
}

分布式锁初始化和消息发布实现

@Component
public class RedisDistributedLock {

	public static Object lock = new Object();

	private static final String LOCK_SUCCESS = "OK";

	private static final String SET_IF_NOT_EXIST = "NX";

	private static final String SET_WITH_EXPIRE_TIME = "PX";

	private static final Long RELEASE_SUCCESS = 1L;

	/**
	 * 尝试获取分布式锁[记得关闭连接]
	 * 
	 * @param lockKey
	 *            锁
	 * @param requestId
	 *            请求标识
	 * @param expireTime
	 *            超期时间(ms)
	 * @return 是否获取成功
	 */
	public boolean tryGetDistributedLock(String lockKey, String requestId,int expireTime) {
		Jedis jedis = null;
		try {
			if (XHTSystemConfig.clusterModeForTomcat&&XHTSystemConfig.clusterRedisEnableWrite) {
				jedis = RedisNodeManagerUtil
						.getJedisFromPool(Constants.CLUSTER_ENABLE_WRITE_NAME);
			} else {
				jedis = RedisPool.getJedis();
			}
			// 获得锁
			synchronized(lock){
				String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST,	SET_WITH_EXPIRE_TIME, expireTime);
				if (LOCK_SUCCESS.equals(result)) {
					return true;
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (null != jedis) {
				jedis.close();
			}
		}
		return false;
	}

	/**
	 * 释放分布式锁[记得关闭连接]
	 * 
	 * @param lockKey
	 *            锁
	 * @param requestId
	 *            请求标识
	 * @return 是否释放成功
	 */
	public boolean releaseDistributedLock(String lockKey, String requestId) {
		Jedis jedis = null;
		try {
			if (XHTSystemConfig.clusterModeForTomcat&&XHTSystemConfig.clusterRedisEnableWrite) {
				jedis = RedisNodeManagerUtil
						.getJedisFromPool(Constants.CLUSTER_ENABLE_WRITE_NAME);
			} else {
				jedis = RedisPool.getJedis();
			}
			// 释放锁
			synchronized(lock){
				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(lockKey),
						Collections.singletonList(requestId));
				if (RELEASE_SUCCESS.equals(result)) {
					
					lock.notifyAll();// 通知当前应用
					
					publishNotifyAllThread();// 通知集群其他节点
					
					return true;
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (null != jedis) {
				jedis.close();
			}
		}
		return false;
	}

	/**
	 * 分布式阻塞获取锁
	 * 
	 * @param key
	 */
	public String tryGetDistributedLockByBlocking(String lockKey, String requestId, int seconds) {
		boolean success = tryGetDistributedLock(lockKey, requestId, seconds*1000);
		if (success) {
			return requestId;// 获取成功直接返回
		}
		long waiting = seconds*1000;
		long end = System.currentTimeMillis() + waiting;
		while (!success && waiting >= 0) {
			// 同一个应用接收到通知会释放掉锁--分布式下其他节点暂时没实现通知(ZooKeeper可以实现事件监听,暂不引入)
			synchronized(lock){
				try {
					lock.wait(waiting);//被通知时释放锁解除等待
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			// 尝试获取锁
			success = tryGetDistributedLock(lockKey, requestId, seconds);
			waiting = end - System.currentTimeMillis();
		}
		if (success) {
			return requestId;
		} else {
			return Constants.CLUSTER_LOCK_FAILED;
		}
	}
	
	/**
	 * 初始化分布式锁消息通道订阅线程
	 */
	@PostConstruct
	private void initLockSubscribeThread(){
		Jedis jedis = null;
		// 用同步块解决并发初始化冲突问题
		synchronized(lock){
			if (XHTSystemConfig.clusterModeForTomcat&&XHTSystemConfig.clusterRedisEnableWrite) {
				jedis = RedisNodeManagerUtil
						.getJedisFromPool(Constants.CLUSTER_ENABLE_WRITE_NAME);
			} else {
				jedis = RedisPool.getJedis();
			}
			Subscriber sub = new Subscriber(jedis, new RedisPubSub(), RedisChannel.DISTRIBUTE_LOCK.toString());
			sub.start();
		}
	}
	
	/**
	 * 发布分布式锁释放通知消息线程
	 */
	private void publishNotifyAllThread(){
		Jedis jedis = null;
		if (XHTSystemConfig.clusterModeForTomcat&&XHTSystemConfig.clusterRedisEnableWrite) {
			jedis = RedisNodeManagerUtil
					.getJedisFromPool(Constants.CLUSTER_ENABLE_WRITE_NAME);
		} else {
			jedis = RedisPool.getJedis();
		}
		Publisher pub=new Publisher(jedis, RedisChannel.DISTRIBUTE_LOCK.toString(), RedisChannel.NOTIFYALL.toString());
		pub.start();
	}

}

使用Arthas监控跟踪

官网地址:https://arthas.aliyun.com/doc/index.html

watch

[arthas@18656]$ watch com.forestar.redis.RedisDistributedLock tryGetDistributedLock "{params,returnObj}" -x 2 -b -s -n 10
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 81 ms, listenerId: 7
method=com.forestar.redis.RedisDistributedLock.tryGetDistributedLock location=AtEnter
ts=2021-03-03 09:15:33; [cost=0.038707ms] result=@ArrayList[
    @Object[][
        @String[xht:application:hly-id-lock],
        @String[71f3965f-9429-4104-a475-03eacfd48dec],
        @Integer[5000],
    ],
    null,
]
method=com.forestar.redis.RedisDistributedLock.tryGetDistributedLock location=AtExit
ts=2021-03-03 09:15:33; [cost=2.3058954671954437E10ms] result=@ArrayList[
    @Object[][
        @String[xht:application:hly-id-lock],
        @String[71f3965f-9429-4104-a475-03eacfd48dec],
        @Integer[5000],
    ],
    @Boolean[true],
]
method=com.forestar.redis.RedisDistributedLock.tryGetDistributedLock location=AtEnter
ts=2021-03-03 09:15:43; [cost=0.016312ms] result=@ArrayList[
    @Object[][
        @String[xht:application:hly-id-lock],
        @String[7471d14f-4a60-4011-9623-d42b11e6d78c],
        @Integer[5000],
    ],
    null,
]
method=com.forestar.redis.RedisDistributedLock.tryGetDistributedLock location=AtExit
ts=2021-03-03 09:15:43; [cost=2.3058964472276306E10ms] result=@ArrayList[
    @Object[][
        @String[xht:application:hly-id-lock],
        @String[7471d14f-4a60-4011-9623-d42b11e6d78c],
        @Integer[5000],
    ],
    @Boolean[true],
]
method=com.forestar.redis.RedisDistributedLock.tryGetDistributedLock location=AtEnter
ts=2021-03-03 09:15:46; [cost=0.016448ms] result=@ArrayList[
    @Object[][
        @String[xht:application:hly-id-lock],
        @String[9132dcb6-b999-4fdc-902e-f834acd6a029],
        @Integer[5000],
    ],
    null,
]
method=com.forestar.redis.RedisDistributedLock.tryGetDistributedLock location=AtExit
ts=2021-03-03 09:15:46; [cost=2.3058967163121655E10ms] result=@ArrayList[
    @Object[][
        @String[xht:application:hly-id-lock],
        @String[9132dcb6-b999-4fdc-902e-f834acd6a029],
        @Integer[5000],
    ],
    @Boolean[false],
]
method=com.forestar.redis.RedisDistributedLock.tryGetDistributedLock location=AtEnter
ts=2021-03-03 09:15:51; [cost=0.011194ms] result=@ArrayList[
    @Object[][
        @String[xht:application:hly-id-lock],
        @String[9132dcb6-b999-4fdc-902e-f834acd6a029],
        @Integer[5],
    ],
    null,
]

trace

[arthas@18656]$ trace com.forestar.xht.hly.XHRYCtrl hly_add 
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 319 ms, listenerId: 10
`---ts=2021-03-03 09:33:07;thread_name=http-nio-8081-exec-7;id=27;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.WebappClassLoader@432cd66e
    `---[2.241784ms] com.forestar.xht.hly.XHRYCtrl$$EnhancerByCGLIB$$9eca3052:hly_add()
        `---[2.068124ms] net.sf.cglib.proxy.MethodInterceptor:intercept() #95
            `---[1.930108ms] com.forestar.xht.hly.XHRYCtrl:hly_add()
                +---[0.036573ms] data.common.util.StringUtils:IsNullOrEmpty() #183
                +---[0.01407ms] com.forestar.xht.util.Utils:getWorkSpace() #189
                +---[0.044448ms] com.forestar.xht.util.Utils:getMetadataWorkspace() #192
                +---[0.017292ms] data.metadata.IMetadataWorkspace:OpenTable() #95
                +---[0.00912ms] com.forestar.xht.util.Utils:getBaseDataService() #193
                +---[0.183323ms] com.alibaba.fastjson.JSONObject:parseObject() #196
                +---[0.011264ms] data.general.RowBase:<init>() #198
                +---[0.010371ms] data.general.RowBase:setCurrentObjects() #199
                +---[0.008256ms] com.forestar.core.session.SessionFactory:getSession() #201
                +---[0.010008ms] com.forestar.core.session.ISession:getTableSuffix() #95
                +---[0.037512ms] data.general.RowBase:getCurrentObjects() #202
                +---[0.010109ms] com.forestar.syscore.encrypt.MD5:<init>() #203
                +---[0.006698ms] data.general.RowBase:getCurrentObjects() #204
                +---[0.034949ms] com.forestar.syscore.encrypt.MD5:getMD5() #95
                +---[0.008434ms] data.general.RowBase:getCurrentObjects() #206
                +---[0.007273ms] data.general.QueryFilter:<init>() #207
                +---[0.024029ms] data.general.QueryFilter:setWhereString() #208
                `---[0.77195ms] com.forestar.redis.RedisDistributedLock:tryGetDistributedLockByBlocking() #215

`---ts=2021-03-03 09:33:16;thread_name=http-nio-8081-exec-35;id=52;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.WebappClassLoader@432cd66e
    `---[1.0833ms] com.forestar.xht.hly.XHRYCtrl$$EnhancerByCGLIB$$9eca3052:hly_add()
        `---[1.019633ms] net.sf.cglib.proxy.MethodInterceptor:intercept() #95
            `---[0.954022ms] com.forestar.xht.hly.XHRYCtrl:hly_add()
                +---[0.007426ms] data.common.util.StringUtils:IsNullOrEmpty() #183
                +---[0.038057ms] com.forestar.xht.util.Utils:getWorkSpace() #189
                +---[0.00616ms] com.forestar.xht.util.Utils:getMetadataWorkspace() #192
                +---[0.007478ms] data.metadata.IMetadataWorkspace:OpenTable() #95
                +---[0.007375ms] com.forestar.xht.util.Utils:getBaseDataService() #193
                +---[0.038762ms] com.alibaba.fastjson.JSONObject:parseObject() #196
                +---[0.005113ms] data.general.RowBase:<init>() #198
                +---[0.006275ms] data.general.RowBase:setCurrentObjects() #199
                +---[0.006229ms] com.forestar.core.session.SessionFactory:getSession() #201
                +---[0.006964ms] com.forestar.core.session.ISession:getTableSuffix() #95
                +---[0.004853ms] data.general.RowBase:getCurrentObjects() #202
                +---[0.005499ms] com.forestar.syscore.encrypt.MD5:<init>() #203
                +---[0.004376ms] data.general.RowBase:getCurrentObjects() #204
                +---[0.019182ms] com.forestar.syscore.encrypt.MD5:getMD5() #95
                +---[0.004806ms] data.general.RowBase:getCurrentObjects() #206
                +---[0.005104ms] data.general.QueryFilter:<init>() #207
                +---[0.004758ms] data.general.QueryFilter:setWhereString() #208
                `---[0.511894ms] com.forestar.redis.RedisDistributedLock:tryGetDistributedLockByBlocking() #215

`---ts=2021-03-03 09:33:25;thread_name=http-nio-8081-exec-15;id=2f;is_daemon=true;priority=5;TCCL=org.apache.catalina.loader.WebappClassLoader@432cd66e
    `---[0.95822ms] com.forestar.xht.hly.XHRYCtrl$$EnhancerByCGLIB$$9eca3052:hly_add()
        `---[0.920732ms] net.sf.cglib.proxy.MethodInterceptor:intercept() #95
            `---[0.866213ms] com.forestar.xht.hly.XHRYCtrl:hly_add()
                +---[0.007652ms] data.common.util.StringUtils:IsNullOrEmpty() #183
                +---[0.011245ms] com.forestar.xht.util.Utils:getWorkSpace() #189
                +---[0.005283ms] com.forestar.xht.util.Utils:getMetadataWorkspace() #192
                +---[0.007431ms] data.metadata.IMetadataWorkspace:OpenTable() #95
                +---[0.007284ms] com.forestar.xht.util.Utils:getBaseDataService() #193
                +---[0.039893ms] com.alibaba.fastjson.JSONObject:parseObject() #196
                +---[0.005045ms] data.general.RowBase:<init>() #198
                +---[0.004925ms] data.general.RowBase:setCurrentObjects() #199
                +---[0.005901ms] com.forestar.core.session.SessionFactory:getSession() #201
                +---[0.008045ms] com.forestar.core.session.ISession:getTableSuffix() #95
                +---[0.00491ms] data.general.RowBase:getCurrentObjects() #202
                +---[0.005447ms] com.forestar.syscore.encrypt.MD5:<init>() #203
                +---[0.004425ms] data.general.RowBase:getCurrentObjects() #204
                +---[0.017348ms] com.forestar.syscore.encrypt.MD5:getMD5() #95
                +---[0.004585ms] data.general.RowBase:getCurrentObjects() #206
                +---[0.004571ms] data.general.QueryFilter:<init>() #207
                +---[0.004564ms] data.general.QueryFilter:setWhereString() #208
                `---[0.431266ms] com.forestar.redis.RedisDistributedLock:tryGetDistributedLockByBlocking() #215

tt

获取类的静态字段、调用类的静态方法:https://arthas.aliyun.com/doc/tt.html

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值