简介
- 在单机环境中,java提供了很多并发的API,但是这些API仅限于同一节点上的应用,对分布式场景无能为力。
- 要求:
- 一把锁在同一集群中只能被一个节点上的一个线程获得,且被该线程释放。
- 可重入,阻塞锁。
- 获取和释放锁高可用(集群,超时)、高效。
- 较常用的几种方案:
- 基于数据库实现(简单)。
- 基于redis等缓存实现(高效)。
- 基于zookeeper实现(可靠)。
mysql
实现
- 利用数据库的行锁和唯一键保证锁只会被一个线程获取。
- 一条记录作为一个锁,主键作为锁的Key。
- insert获取锁,delete释放锁。参考。
CREATE TABLE methodLock (
id varchar(64) NOT NULL COMMENT '主键',
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (id),
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
特点
- 优点:
- 缺点:
- 没有失效时间(可以通过定时任务清数据解决)。
- 非阻塞(while重试)。
- 非重入,且无法保证是持有锁的线程删除锁(增加字段记录节点和线程信息,insert之前先select)。
- 操作数据库,性能不高。
原理
redis
实现
- redis基本使用。
- 利用redis的单线程保证只有一个线程获取锁。
- key-value作为锁。
- set获取锁:requestId标识进程,NX只在key不存在时设置,PX和expireTime设置毫秒级的超时时间。
- del删除锁:校验是当前进程的锁之后再删除。lua脚本解锁。
- jedis 非线程安全 ,需要线程持有自己的对象作为入参。多线程异常。多线程中使用jedis。
@Service
public class RedisLockServiceImpl {
@Autowired
private JedisPool jedisPool;
public Jedis getJedis() {
return jedisPool.getResource();
}
public boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
public boolean unlock(Jedis jedis, String lockKey, String requestId) {
Long result = null;
if (requestId.equals(jedis.get(lockKey))) {
result = jedis.del(lockKey);
}
return Objects.equals(result, 1L);
}
}
特点
- 优点:
- 高性能,实现较简单。
- 可设置失效时间。
- 校验value保证是持有锁的线程释放锁。
- 缺点:
- 非阻塞(while重试)。
- 非重入(set之前查询value是否为对应的线程)。
原理
zookeeper
实现
@Service
public class ZkLockServiceImpl {
private Logger logger = LoggerFactory.getLogger(ZkLockServiceImpl.class);
private CuratorFramework client;
private String zkAddress = "xx.xx.xx.xx:2181";
private Map<String, InterProcessMutex> interProcessMutexMap = Maps.newConcurrentMap();
@PostConstruct
private void init() {
try {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(10000, 6);
client = CuratorFrameworkFactory.newClient(zkAddress, 10000, 180000, retryPolicy);
client.start();
InterProcessMutex mutex = new InterProcessMutex(client, LockConst.ZK_TEST_LOCK);
interProcessMutexMap.put(LockConst.ZK_TEST_LOCK, mutex);
} catch (Exception e) {
logger.error("ZkLockServiceImpl init failed! e : {}", e);
}
}
public boolean tryLock(String path) {
InterProcessMutex mutex = interProcessMutexMap.get(path);
try {
return mutex.acquire(2000L, TimeUnit.MILLISECONDS);
} catch (Exception e) {
logger.error("ZkLockServiceImpl tryLock error! e : {}", e);
return false;
}
}
public boolean unlock(String path) {
try {
InterProcessMutex mutex = interProcessMutexMap.get(path);
if (mutex.isAcquiredInThisProcess()) {
mutex.release();
} else {
logger.warn(Thread.currentThread().getName() + ": not in this process");
}
} catch (Exception e) {
logger.error("ZkLockServiceImpl release error! e : {}", e);
return false;
}
return true;
}
}
特点
- 优点:
- 可设置失效时间,并且节点挂掉之后会删除该临时节点。
- isAcquiredInThisProcess保证是持有锁的线程释放锁。
- InterProcessMutex实现重入,InterProcessSemaphoreMutex实现非重入。
- 缺点:
- 需要创建和销毁节点,性能不够高。
- 非阻塞(while重试)。
原理
- 实现原理:
- 锁节点创建临时顺序节点,如果是序号最小的子节点,获取锁。
- 路径:/lock/mutex/aries-check/lock/multi/new/phb0/_c_623d03a6-c7c0-4ae2-aca9-6cef35567815-lock-0000000063
- 如果获取锁失败,添加watcher监听上一个顺序节点。
- 上一个顺序节点被删除后尝试获取锁或者获取锁超时失败。
- 参考。