传统方式,通过setnx实现,不多说直接上代码;
```
//查询或设置锁
public String acquireLock(Jedis conn, String lockName) {
return acquireLock(conn, lockName, 10000);
}
public String acquireLock(Jedis conn, String lockName, long acquireTimeout) {
String identifier = UUID.randomUUID().toString();
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx("lock:" + lockName, identifier) == 1) {
return identifier;
}
try {
Thread.sleep(1);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
return null;
}
“`
//释放锁
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
String lockKey = "lock:" + lockName;
while (true){
conn.watch(lockKey);
if (identifier.equals(conn.get(lockKey))){
Transaction trans = conn.multi();
trans.del(lockKey);
List<Object> results = trans.exec();
if (results == null){
continue;
}
return true;
}
conn.unwatch();
break;
}
return false;
}
//多线程触发锁进行测试
public class PollQueueThread
extends Thread
{
private Jedis conn;
private boolean quit;
private Gson gson = new Gson();
public PollQueueThread(){
this.conn = new Jedis("localhost");
this.conn.select(15);
}
public void quit() {
quit = true;
}
public void run() {
while (!quit){
Set<Tuple> items = conn.zrangeWithScores("delayed:", 0, 0);
Tuple item = items.size() > 0 ? items.iterator().next() : null;
if (item == null || item.getScore() > System.currentTimeMillis()) {
try{
sleep(10);
}catch(InterruptedException ie){
Thread.interrupted();
}
continue;
}
String json = item.getElement();
String[] values = gson.fromJson(json, String[].class);
String identifier = values[0];
String queue = values[1];
String locked = acquireLock(conn, identifier);
if (locked == null){
continue;
}
if (conn.zrem("delayed:", json) == 1){
conn.rpush("queue:" + queue, json);
}
releaseLock(conn, identifier, locked);
}
}
}
这种方式的缺点很明显, 每次循环都要sleep(10),这样对服务器以及redis都带来了多余的压力;
查了下资料,redis的java客户端工具redisson,它内部包含的lock、tryLock等方法可以优雅的解决这个问题;
maven中引入redisson的jar包,目前暂时无法直接用XML配置的方式配置连接池或者主从redis等参数,所以只能手写一个分布式锁的连接客户端;
package com.caocao.util.redis;
import org.apache.commons.lang.StringUtils;
import org.redisson.Config;
import org.redisson.MasterSlaveServersConfig;
import org.redisson.Redisson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component("disLockClientTemplate")
public class DisLockClientTemplate {
private static final Logger log = LoggerFactory.getLogger(DisLockClientTemplate.class);
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Value("${redis.slaves}")
private String slaves;
@Value("${redis.password}")
private String password;
private Redisson redisson;
public DisLockClientTemplate() {
}
@PostConstruct
public void initRedisson() {
Config config = new Config();
MasterSlaveServersConfig mssConf = config.useMasterSlaveConnection().setMasterAddress(host+":"+port);
mssConf.setPassword(password);
//集群部署redis时获取slaves,暂时保留
/*if (StringUtils.isNotBlank(slaves)) {
for (String slave : slaves.split(",")) {
mssConf.addSlaveAddress(slave);
}
}*/
redisson = Redisson.create(config);
log.info("init Redisson DONE!");
}
public Redisson getRedisson() {
return redisson;
}
}
调用锁
public class TestDisLock1 {
public static void main(String[] args) {
String filePath = "classpath:conf/applicationContext.xml";
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(filePath);
context.start();
DisLockClientTemplate disLockClientTemplate = (DisLockClientTemplate) context.getBean("disLockClientTemplate");
Redisson redisson = disLockClientTemplate.getRedisson();
RLock lock = redisson.getLock("haogrgr");
try {
lock.tryLock(1000,2000, TimeUnit.MILLISECONDS);
System.out.println("testLock2");
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
这里有同学会问,那redisson如何实现Lock锁的呢?跟第一种比有什么好处?
具体的实现类是RedissonLock, 看看实现原理. 先看看中例子执行时, 所运行的命令(通过monitor命令):
127.0.0.1:6379> monitor
OK
1434959509.494805 [0 127.0.0.1:57911] "SETNX" "haogrgr" "{\"@class\":\"org.redisson.RedissonLock$LockValue\",\"counter\":1,\"id\":\"c374addc-523f-4943-b6e0-c26f7ab061e3\",\"threadId\":1}"
1434959509.494805 [0 127.0.0.1:57911] "GET" "haogrgr"//锁定
1434959509.524805 [0 127.0.0.1:57911] "MULTI"
1434959509.529805 [0 127.0.0.1:57911] "DEL" "haogrgr"//释放锁
1434959509.529805 [0 127.0.0.1:57911] "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"//通知其他等待线程
1434959509.529805 [0 127.0.0.1:57911] "EXEC"
可以看到, 大概原理是, 通过判断Redis中是否有某一key, 来判断是加锁还是等待, 最后的publish是一个解锁后, 通知阻塞在lock的线程.
分布式锁的实现依赖的单点, 这里Redis就是单点, 通过在Redis中维护状态信息来实现全局的锁. 那么来看看RedissonLock如何
实现可重入, 保证原子性等等细节.
Lock源码
public void lock() {
try {
lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
public void lockInterruptibly() throws InterruptedException {
lockInterruptibly(-1, null); //leaseTime : -1 表示key不设置过期时间
}
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
Long ttl;
if (leaseTime != -1) {
ttl = tryLockInner(leaseTime, unit);
} else {
ttl = tryLockInner();
}
// lock acquired
if (ttl == null) {
return;
}
subscribe().awaitUninterruptibly();
try {
while (true) {
if (leaseTime != -1) {
ttl = tryLockInner(leaseTime, unit);
} else {
ttl = tryLockInner();
}
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
RedissonLockEntry entry = ENTRIES.get(getEntryName());
if (ttl >= 0) {
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
entry.getLatch().acquire();
}
}
} finally {
unsubscribe();
}
}
tryLockInner源码
private Long tryLockInner() {
//保存锁的状态: 客户端UUID+线程ID来唯一标识某一JVM实例的某一线程
final LockValue currentLock = new LockValue(id, Thread.currentThread().getId());
//用来保存重入次数, 实现可重入功能, 初始情况是1
currentLock.incCounter();
//Redisson封装了交互的细节, 具体的逻辑为execute方法逻辑.
return connectionManager.write(getName(), new SyncOperation<LockValue, Long>() {
@Override
public Long execute(RedisConnection<Object, LockValue> connection) {
//如果key:haogrgr不存在, 就set并返回true, 否则返回false
Boolean res = connection.setnx(getName(), currentLock);
//如果设置失败, 那么表示有锁竞争了, 于是获取当前锁的状态, 如果拥有者是当前线程, 就累加重入次数并set新值
if (!res) {
//通过watch命令配合multi来实现简单的事务功能
connection.watch(getName());
LockValue lock = (LockValue) connection.get(getName());
//LockValue的equals实现为比较客户id和threadid是否一样
if (lock != null && lock.equals(currentLock)) {
//如果当前线程已经获取过锁, 则累加加锁次数, 并set更新
lock.incCounter();
connection.multi();
connection.set(getName(), lock);
if (connection.exec().size() == 1) {
return null; //set成功,
}
}
connection.unwatch();
//走到这里, 说明上面set的时候, 其他客户端在 watch之后->set之前 有其他客户端修改了key值
//则获取key的过期时间, 如果是永不过期, 则返回-1, 具体处理后面说明
Long ttl = connection.pttl(getName());
return ttl;
}
return null;
}
});
}
tryLockInner的逻辑已经看完了, 可以知道, 有三种情况:
(1) key不存在, 加锁:
当key不存在时, 设置锁的初始状态并set, 具体来看就是 setnx haogrgr LockValue{ id: Redisson对象的id, threadId: 当前线程id, counter: 当前重入次数,这里为第一次获取,所以为1
通过上面的操作. 达到获取锁的目的, 通过setnx来达到实现类似于 if(map.get(key) == null) { map.put(key) } 的功能, 防止多个客户端同时set时, 新值覆盖老值.
(2)key存在, 且获取锁的当前线程, 重入:
这里就是锁重入的情况, 也就是锁的拥有者第二次调用lock方法, 这时, 通过先get, 然后比较客户端ID和当前线程ID来判断拥有锁的线程是不是当前线程.(客户端ID+线程ID才能唯一定位锁拥有者线程)
判断发现当前是重入情况, 则累加LockValue的counter, 然后重新set回去, 这里使用到了watch和multi命令, 防止 get -> set 期间其他客户端修改了key的值.
(3)key存在, 且是其他线程获取的锁, 等待:
首先尝试获取锁(setnx), 失败后发现锁拥有者不是当前线程, 则获取key的过期时间, 返回过期时间
那么接下来看看tryLockInner调用完成后的代码.
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
Long ttl;
if (leaseTime != -1) {
ttl = tryLockInner(leaseTime, unit);
} else {
ttl = tryLockInner(); //lock()方法调用会走的逻辑
}
// lock acquired
//加锁成功(新获取锁, 重入情况) tryLockInner会返回null, 失败会返回key超时时间, 或者-1
(key未设置超时时间)
if (ttl == null) {
//加锁成功, 返回
return;
}
//subscribe这个方法代码有点多, Redisson通过netty来和redis通讯, 然后subscribe返回的是一个Future类型,
//Future的awaitUninterruptibly()调用会阻塞, 然后Redisson通过Redis的pubsub来监听unlock的topic(getChannelName())
//例如, 5中所看到的命令 "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"
//当解锁时, 会向名为 getChannelName() 的topic来发送解锁消息("0")
//而这里 subscribe() 中监听这个topic, 在订阅成功时就会唤醒阻塞在awaitUninterruptibly()的方法.
//所以线程在这里只会阻塞很短的时间(订阅成功即唤醒, 并不代表已经解锁)
subscribe().awaitUninterruptibly();
try {
while (true) { //循环, 不断重试lock
if (leaseTime != -1) {
ttl = tryLockInner(leaseTime, unit);
} else {
ttl = tryLockInner(); //不多说了
}
// lock acquired
if (ttl == null) {
break;
}
// 这里才是真正的等待解锁消息, 收到解锁消息, 就唤醒, 然后尝试获取锁, 成功返回, 失败则阻塞在acquire().
// 收到订阅成功消息, 则唤醒阻塞上面的subscribe().awaitUninterruptibly();
// 收到解锁消息, 则唤醒阻塞在下面的entry.getLatch().acquire();
RedissonLockEntry entry = ENTRIES.get(getEntryName());
if (ttl >= 0) {
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
entry.getLatch().acquire();
}
}
} finally {
unsubscribe(); //加锁成功或异常,解除订阅``
}
}