一、什么是锁?
在单进程的应用中,如果存在多个线程修改某个共同的资源,就需要对方法或者代码块进行同步,防止出现线程安全问题。代码块和方法的同步就是通过锁来进行,保证同一时刻只能有一个线程来修改该资源。
二、什么是分布式锁?
在分布式系统中,不再是单个进程中的多个线程竞争锁,而是不同进程对锁的竞争,这时普通的锁就不起作用了,需要用到分布式锁。
三、分布式锁应该要具有的几个特点
1.互斥性:保证在分布式部署的应用集群中,同一个锁同一时间只能被一台机器上的一个线程获取,保证代码的执行顺序。
2.可重入性:避免出现死锁的情况。
3.阻塞性:在某个线程获取锁失败后,能够等待重新获取,而不是要重新执行获取锁的代码。
4.具有高性能和高可用的获取锁、释放锁的能力。
四、两种常见是分布式锁实现方式
1.基于redis
使用jedis的方法jedis.set(key, value, nxxx, expx, time) 设置key和value,并设置有效的时间,这样就是实现加锁的功能。如果需要解锁就验证key和value是否相同,就能解锁。
第三个参数设置NX,这样能保证如果该key存在,该方法就不会执行成功。
可以用UUID来作为value,作为同一个方法请求的标志,这样就能保证唯一性。
如果获取锁后程序突然崩溃,到达过期时间后也会将锁释放。
释放锁的时候要通过执行一个lua脚本来解锁,代码如下
//如果是采取先验证key和value然后解锁,会造成不是原子操作,而将其他方法加的锁解开 public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (result == 1) { return true; } return false; }
获取锁的时候要进行循环去获取锁,因为不一定一次就能获取锁。
2.基于zookeeper
zookeeper通过创建临时节点来实现分布式锁。大概思路就是所有要获取同一个锁的线程到同一个节点下面去创建子节点,谁的节点序号最小,谁就获得了锁。如果要释放锁,则只需要删除锁即可。由于是临时节点,如果应用出现问题,临时节点会自动删除,
其他客户端也就能获得锁了。
zookeeper可以实现阻塞的锁,通过在节点上绑定监听器,如果节点有变动,则通知客户端检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁来执行业务逻辑了。
zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,要再次获取锁的时候就对比一下信息是否和最小节点的信息一样,一样就能获取,不一样就重新创建节点排队。
五、悲观锁与乐观锁
悲观锁:顾名思义对并发引起的数据安全问题持悲观态度,每次去操作数据的时候都认为别人会修改,所以每次操作数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
乐观锁:顾名思义对并发引起的数据安全问题持乐观态度,每次去操作数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。