细说分布式锁

一、使用场景

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。

分布式锁有以下几个特点:

  1. 互斥性:和我们本地锁一样互斥性是最基本的,但是分布式锁需要保证在不用节点的不同线程的互斥。
  2. 可重入性:同一个节点上的同一个线程如果获取锁之后那么也可以再次获取这个锁。
  3. 锁超时:和本地锁一样支持锁超时,防止死锁。
  4. 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
  5. 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
  6. 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。

常见的分布式锁:
我们一般实现分布式锁有以下几个方式:
1.MySQL
2.ZK
3.Redis
4.自研分布式锁,如谷歌的Chubby

二、mysql数据库的实现

2.1利用mysql的隔离性:唯一索引

创建一张锁表

  1. CREATE TABLE `methodLock` ( 
  2. `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键'
  3. `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名'
  4. `desc` varchar(1024) NOT NULL DEFAULT '备注信息'
  5. `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成'
  6. PRIMARY KEY (`id`), 
  7. UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE 
  8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'

当我们想要获取锁的时候,执行以下sql:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

因为我们对method_name字段做了唯一性约束,所以如果有多个插入操作,数据库只会保证一个成功其他的抛出异常,我们可以认为操作成功的那个线程获得锁,可以执行方法体的内容。
当我们想释放锁的时候,需要执行以下sql:

delete from methodLock where method_name ='method_name'

2.2基于数据库排他锁

还用刚刚创建的那张表,可以通过数据库的排他锁来实现分布式锁。

public boolean lock(){
    connection.setAutoCommit(false)
    while(true){
        try{
            result = select * from methodLock where method_name=xxx for update;
            if(result==null){
                return true;
            }
        }catch(Exception e){

        }
        sleep(1000);
    }
    return false;
}

在查询语句后面加for update,数据库会在查询过程中给数据库加排他锁(InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
获得排他锁的线程即可获得分布式锁,再通过以下方法解锁。

public void unlock(){
    connection.commit();
}

2.3version乐观锁

乐观锁与前面最大的区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。一般抢购、秒杀就是用了这种实现以防止超卖。
通过增加递增的版本号字段实现乐观锁
select ....,version
update table set vesion+1 where version=XX

优点:
直接借助数据库,实现简单。
缺点:
数据库是单点,不够可靠。
锁没有失效时间
影响数据库性能
使用数据库的行级锁不一定靠谱,尤其当锁表并不大的时候。

三、Redis的实现

Redis可以利用命令Setnx()来实现分布式锁,性能是最好的,但是可靠性没有zookeeper好,而且通过超时时间来控制锁的失效时间并不可靠。
还可以通过Lua脚本来释放锁,这种分布式锁在redis sentinel集群情况下并不靠谱。
具体可以参考文章https://www.cnblogs.com/demingblog/p/9542124.html#%E9%94%81%E8%B6%85%E6%97%B6

  1. package cn.sp.lock; 
  2.  
  3. import org.springframework.beans.factory.annotation.Autowired; 
  4. import org.springframework.dao.DataAccessException; 
  5. import org.springframework.data.redis.core.RedisOperations; 
  6. import org.springframework.data.redis.core.SessionCallback; 
  7. import org.springframework.data.redis.core.StringRedisTemplate; 
  8. import org.springframework.lang.Nullable; 
  9. import org.springframework.stereotype.Component; 
  10.  
  11. import java.util.List; 
  12. import java.util.UUID; 
  13. import java.util.concurrent.TimeUnit; 
  14.  
  15. /** 
  16. * Created by 2YSP on 2019/1/26. 
  17. */ 
  18. @Component 
  19. public class RedisLock
  20.  
  21. @Autowired 
  22. private StringRedisTemplate redisTemplate; 
  23.  
  24.  
  25. /** 
  26. * 获取锁 
  27. * @param lockName 
  28. * @param requireTimeOut 
  29. * @return 
  30. */ 
  31. public String requireLock(String lockName,long requireTimeOut)
  32. String key = "lock:"+lockName; 
  33. String identifier = UUID.randomUUID().toString(); 
  34. long end = System.currentTimeMillis() + requireTimeOut; 
  35. while (System.currentTimeMillis() < end){ 
  36. Boolean success = redisTemplate.opsForValue().setIfAbsent(key, identifier); 
  37. if (success){ 
  38. return identifier; 
  39.  
  40. try
  41. Thread.sleep(10); 
  42. } catch (InterruptedException e) { 
  43. e.printStackTrace(); 
  44. return null
  45.  
  46. /** 
  47. * 释放锁 
  48. * @param lockName 
  49. * @param identifier 
  50. * @return 
  51. */ 
  52. public boolean releaseLock(String lockName,String identifier)
  53. String key = "lock:"+lockName; 
  54. while (true){ 
  55. redisTemplate.watch(key); 
  56. if (identifier.equals(redisTemplate.opsForValue().get(key))){ 
  57. //检查是否还未释放 
  58. SessionCallback<Object> sessionCallback = new SessionCallback<Object>() { 
  59. @Nullable 
  60. @Override 
  61. public Object execute(RedisOperations operations) throws DataAccessException
  62. operations.multi(); 
  63. operations.delete(key); 
  64. List obj = operations.exec(); 
  65. return obj; 
  66. }; 
  67. Object object = redisTemplate.execute(sessionCallback); 
  68. if (object != null) { 
  69. return true
  70. continue
  71. redisTemplate.unwatch(); 
  72. break
  73. return false
  74.  
  75. /** 
  76. * 
  77. * @param lockName 
  78. * @param requireTimeOut 
  79. * @return 
  80. */ 
  81. public String requireLockWithTimeOut(String lockName,long requireTimeOut,long lockTimeOut)
  82. String key = "lock:"+lockName; 
  83. String identifier = UUID.randomUUID().toString(); 
  84. long end = System.currentTimeMillis() + requireTimeOut; 
  85. int lockExpire = (int) (lockTimeOut/1000); 
  86. while (System.currentTimeMillis() < end){ 
  87. Boolean success = redisTemplate.opsForValue().setIfAbsent(key, identifier); 
  88. if (success){ 
  89. //设置过期时间 
  90. redisTemplate.expire(key,lockExpire, TimeUnit.SECONDS); 
  91. return identifier; 
  92.  
  93. if (redisTemplate.getExpire(key,TimeUnit.SECONDS) == -1){ 
  94. redisTemplate.expire(key,lockExpire, TimeUnit.SECONDS); 
  95. try
  96. Thread.sleep(10); 
  97. } catch (InterruptedException e) { 
  98. e.printStackTrace(); 
  99. return null
  100.  

四、zookeeper的实现

zookeeper实现分布式锁的原理主要是利用顺序临时节点的特性。
获取锁
所有客户端都试图创建同一个临时节点A,zookeeper会保证所有客户端中只有一个能创建成功,那么就可以认为该客户端获得了锁,其他客户端就要到临时节点A上注册一个子节点变更的Watcher监听。
释放锁
以下两种情况,都可能释放锁:

  1. 当前获取锁的客户端机器发生宕机,那么zookeeper上的这个临时节点就会被删除。
  2. 正常业务逻辑执行完后,客户端会主动将自己创建的临时节点删除。

无论什么情况移除了节点A,ZooKeeper都会通知所有在该节点上注册了子节点变更Watcher的客户端,这些客户端在接收到通知后,再次重新发起分布式锁获取。

我是使用的开源客户端Curator实现的
pom.xml

  1. <dependencies> 
  2. <dependency> 
  3. <groupId>org.springframework.boot</groupId> 
  4. <artifactId>spring-boot-starter-web</artifactId> 
  5. </dependency> 
  6.  
  7. <dependency> 
  8. <groupId>org.springframework.boot</groupId> 
  9. <artifactId>spring-boot-starter-test</artifactId> 
  10. <scope>test</scope> 
  11. </dependency> 
  12.  
  13. <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes --> 
  14. <dependency> 
  15. <groupId>org.apache.curator</groupId> 
  16. <artifactId>curator-recipes</artifactId> 
  17. <version>2.11.0</version> 
  18. </dependency> 
  19. </dependencies> 

HelloController.java

package cn.sp.controller;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by 2YSP on 2019/1/27.
 */
@RestController
@RequestMapping("/zoo")
public class HelloController {

    public final Logger log = LoggerFactory.getLogger(this.getClass());

    @Autowired
    CuratorFramework client;

    @RequestMapping("/hello")
    public String hello(){
        final InterProcessMutex lock = new InterProcessMutex(client, "/lock");
        try {
            lock.acquire();
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
        log.info("{} 获取锁成功",Thread.currentThread().getName());
        System.out.println("执行业务逻辑。。。。");
        try {
            lock.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("{} 释放锁成功",Thread.currentThread().getName());
        return "OK";
    }
}

ZookeeperLockApplication.java

@SpringBootApplication
public class ZookeeperLockApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZookeeperLockApplication.class, args);
    }

    @Bean
    public CuratorFramework client(){
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("198.13.40.234:2181,198.13.40.234:2182,198.13.40.234:2183")
                .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();
        client.start();
        return client;
    }

}

然后使用ab测试访问即可。

注意:
Curator的版本要跟ZooKeeper的版本对应好,不然会报错。

Curator 2.x.x - compatible with both ZooKeeper 3.4.x and ZooKeeper 3.5.x

Curator 3.x.x - compatible only with ZooKeeper 3.5.x and includes support for new features such as dynamic reconfiguration, etc.

zookeeper有较好的性能和可靠性,但是性能不如Redis,主要原因是写操作(获取锁释放锁)都需要在Leader上进行,然后同步至follower。

五、总结

相关代码已上传至github,点击这里访问

从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

posted @ 2019-02-16 19:56  烟味i  阅读(888)  评论(0编辑  收藏  举报