项目中使用到了redis如果这块没有严格要求,可以直接采用REDIS分布式锁。 在单体应用中直接通过线程notifyAll()实现线程之间协作,分布式中需要通过事件通知或消息发布订阅来实现。
目录
缺点与参考实现
优点:性能高,实现方便,在允许偶发的锁失效情况下,不影响系统正常使用建议使用缓存方式的锁。
缺点:通过锁超时机制不是十分可靠,当线程获得锁后,因处理时间很长导致锁超时,锁即失效。
参考实现:
Java中wait、notify、notifyAll使用详解
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