文章首发于公众号:松花皮蛋的黑板报
作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解
一般情况下我们会通过下面的方法进行资源的一致性保护
// THIS CODE IS BROKEN
function writeData(filename, data) {
var lock = lockService.acquireLock(filename);
if (!lock) {
throw 'Failed to acquire lock';
}
try {
var file = storage.readFile(filename);
var updated = updateContents(file, data);
storage.writeFile(filename, updated);
} finally {
lock.release();
}
}
但是很遗憾的是,上面这段代码是不安全的,比如客户端client-1获取锁后由于执行垃圾回收GC导致一段时间的停顿(stop-the-word GC pause)或者其他长时间阻塞操作,此时锁过期了,其他客户如client-2会获得锁,当client-1恢复后就会出现client-1client-2同时处理获得锁的状态
我们可能会想到通过令牌或者叫版本号的方式,然而在使用Redis作为锁服务时并不能解决上述的问题。不管我们怎么修改Redlock生成token的算法,使用unique random随机数是不安全的,使用引用计数也是不安全的,一个redis node服务可能会出宕机,多个redis node服务可能会出现同步异常(go out of sync)。Redlock锁会失效的根本原因是Redis使用getimeofday作为key缓存失效时间而不是监视器(monitonic lock),服务器的时钟出现异常回退无法百分百避免,ntp分布式时间服务也是个难点
分布式锁实现需要考虑锁的排它性和不能释放它人的锁,作者不推荐使用Redlock算法,推荐使用zookeeper或者数据库事务(个人不推荐:for update性能太差了)
补充:使用zookeeper实现分布式锁
可以通过客户端尝试创建节点路径,成功就获得锁,但是性能较差。更好的方式是利用zookeeper有序临时节点,最小序列获得锁,其他节点lock时需要阻塞等待前一个节点(比自身序列小的最大那个)释放锁(countDownLatch.wait()),当触发watch事件时将计数器减一(countDownLatch.countDown()),然后此时最小序列节点将会获得锁。可以利用Curator简化操作,示例如下
public static void main(String[] args) throws Exception {
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//创建工厂连接
final CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString(connetString)
.sessionTimeoutMs(sessionTimeOut).retryPolicy(retryPolicy).build();
curatorFramework.start();
//创建分布式可重入排他锁,监听客户端为curatorFramework,锁的根节点为/locks
final InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/lock");
final CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatch.await();
//加锁
mutex.acquire();
process();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
//释放锁
mutex.release();
System.out.println(Thread.currentThread().getName() + ": release lock");
} catch (Exception e) {
e.printStackTrace();
}
}
}
},"Thread" + i).start();
}
Thread.sleep(100);
countDownLatch.countDown();
}
}
补充:redis实现分布式锁
public enum FreeLockUtil {
instance;
public static FreeLockUtil getInstance()
{
return instance;
}
@Autowired
@Qualifier("jimClient")
private Cluster jimClient;
@Autowired
private TdeUtil tdeUtil;
private String scriptHash;
@PostConstruct
public void init() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
scriptHash = jimClient.scriptLoad(script);
}
/**
* @Description: 没有获得锁时会返回空
* @Param: [key]
* @return: java.lang.String
* @Author: Pidan
*/
public String lock(String lockKey)
{
String token = tdeUtil.random();
//不要将set和expire分开
Boolean lockRes = jimClient.set(lockKey, token, 1L,TimeUnit.MINUTES, false);
return lockRes?token:null;
}
/**
* @Description: 类似CAS版本号
* @Param: [key, value]
* @return: void
* @Author: Pidan
*/
public void unlock(String lockKey,String token)
{
//不要在客户端使用get-if-equals-del
jimClient.evalsha(scriptHash, Collections.singletonList(lockKey),Collections.singletonList(token),true);
}
}
不管是基于Redis或者是Zookeeper实现分布式锁都有各点的优缺点,Redis的高并发是Zookeeper无法比拟的,但是Redis缓存的内存大小如果不足的话极有可能会导致信息丢失,反观使用Zookeeper实现分布式锁,会导致性能开销比较高,因为需要动态创建删除临时节点,频繁操作磁盘读写,不过它的可靠性更高
文章来源:www.liangsonghua.me
作者介绍:京东资深工程师-梁松华,长期关注稳定性保障、敏捷开发、JAVA高级、微服务架构
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。