Redisson实现分布式锁
在分布式系统中,分布式锁是一个很常见的技术。即有很多个进程同时访问同一个共享资源没有同步访问,资源的载体可能是传统关系型数据库或者NoSQL。
如果是在单机环境中,可以使用ReentrantLock或者synchronized代码块来实现,然而这些在分布式环境下却不能满足要求。
例如有这样的一个场景:
多台服务器同时从MQ消息队列接消息,接到消息后的处理逻辑是:根据消息中的业务id去查询数据库,如果数据库中不存在,则插入,反之则更新。
这样就会出现这样一个问题:服务器A和服务器B中接收到的消息中业务id是一样的,2台服务器同时到数据库中查询,都不存在,则都会进行插入操作,最后的结果是表中有2条一样的记录(或者有主键约束的话报主建不唯一异常插入失败),显然这不是我能所期望的!
分布式环境中多台服务器相当于多个独立的jvm环境,有多个进程(注意不是线程)对同一资源进行操作,jdk中用作同步的Lock或者synchronized就会失效。
这里就需要用到分布式锁,目前实现分布式锁的主要技术大概有:
-
基于Redis实现(或者其他缓存memcached,tair),主要基于redis的setnx(set if not exist)命令;
-
基于Zookeeper实现;
-
基于数据库表version字段实现,乐观锁,两个线程可以同时读取到原有的version值,但是最终只有一个可以完成操作;
下面是redis实现分布式锁的方法,主要用到了Redisson,它是redis官方推荐的分布式java客户端,和Jedis相比它实现了分布式和可扩展的java数据结构,
这里有一篇对比文章:Jedis与Redisson选型对比
Redisson的介绍可以到:https://github.com/redisson/redisson/wiki/1.-%E6%A6%82%E8%BF%B0 这里去了解!
具体实现代码:
import org.redisson.Config; import org.redisson.Redisson; import org.redisson.RedissonClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author created by dehong * @date 2018年3月4日 下午4:12:42 * */ public class RedissonManager { private static final Logger logger = LoggerFactory.getLogger(RedissonManager.class); private static RedissonClient redisson; //private static Config config; /** * 通过配置文件初始化 */ static{ logger.info("init redisson ..."); Config config = null; try { config = Config.fromYAML(RedissonManager.class.getClassLoader().getResourceAsStream("redisson.yaml")); //config = Config.fromJSON(RedissonManager.class.getClassLoader().getResourceAsStream("redisson.json")); } catch (Exception e) { logger.error("RedissonClient init failed !", e); } redisson = Redisson.create(config); } /** * 获取Redisson的实例对象 * @return */ public static Redisson getRedisson(){ if(redisson == null){ logger.info("RedissonClient init failed !"); return null; } return (Redisson) redisson; } /** * 测试Redisson是否正常 * @param args */ public static void main(String[] args) { Redisson redisson = RedissonManager.getRedisson(); System.out.println("redisson = " + redisson); } }
上面的RedissonClient初始化是用配置文件yaml的形式,也可以用json,可以去参考官方文档 https://github.com/redisson/redisson
yaml示例(单机redis节点模式)
--- singleServerConfig: idleConnectionTimeout: 10000 pingTimeout: 1000 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500 reconnectionTimeout: 3000 failedAttempts: 3 password: null subscriptionsPerConnection: 5 clientName: null address: - redis://192.168.43.84:6379 subscriptionConnectionMinimumIdleSize: 1 subscriptionConnectionPoolSize: 50 connectionMinimumIdleSize: 32 connectionPoolSize: 64 database: 0 dnsMonitoring: false dnsMonitoringInterval: 5000
工具类:
import java.util.concurrent.TimeUnit; import org.redisson.Redisson; import org.redisson.core.RLock; /** * * @author created by dehong * @date 2018年3月4日 下午4:30:11 * Redisson分布式锁 工具类 */ public class RedissonLockUtil { private static Redisson redisson = RedissonManager.getRedisson(); private static final String LOCK_FLAG = "redissonlock_"; /** * 根据name对进行上锁操作,redissonLock 阻塞事的,采用的机制发布/订阅 * @param key */ public static void lock(String key){ String lockKey = LOCK_FLAG + key; RLock lock = redisson.getLock(lockKey); //lock提供带timeout参数,timeout结束强制解锁,防止死锁 :1分钟 lock.lock(1, TimeUnit.MINUTES); } /** * 根据name对进行解锁操作 * @param key */ public static void unlock(String key){ String lockKey = LOCK_FLAG + key; RLock lock = redisson.getLock(lockKey); //如果锁被当前线程持有,则释放 if(lock.isHeldByCurrentThread()){ lock.unlock(); } } }
注意:上面的unlock()中不加isHeldByCurrentThread()条件的话,在执行的task的时间超过timeout时,此时如果unlock,其实redisson已经主动unlock了,就会出现IllegalMonitorStateException 异常
调用:
try { //获取锁,这里的key可以上面场景中的业务id RedissonLockUtil.lock(key); //具体要执行的代码.... } catch (Exception e) { e.printStackTrace(); }finally { //释放锁 RedissonLockUtil.unlock(key); }