文章首发于公众号:松花皮蛋的黑板报
做者就任于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深刻的理解java
通常状况下咱们会经过下面的方法进行资源的一致性保护node
// 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
咱们可能会想到经过令牌或者叫版本号的方式,然而在使用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实现分布式锁,会致使性能开销比较高,由于须要动态建立删除临时节点,频繁操做磁盘读写,不过它的可靠性更高session
文章来源:www.liangsonghua.me
做者介绍:京东资深工程师-梁松华,长期关注稳定性保障、敏捷开发、JAVA高级、微服务架构架构