从本质上说。
单机锁,锁本地的一个共享资源。
分布式锁,锁一个所有应用访问到的共享资源。
不管是redis锁,还是zookeeper锁,锁的都是共享资源。
在redis上,通过判断一个key是否存在存在作为锁,此时这个key就是共享资源,以key是否存在作为锁的标志,表现在代码中就是setnx是否返回1.
在zookeeper上,通过判断一个node是否存在作为锁,此时这个node就是共享资源,以node是否存在作为锁的标志,表现在代码中就是是否能create这个结点而不报出结点已存在的异常。
只是在这个逻辑可能有一些优化,比如curator框架中内置的interProcessLock,其也是通过判断能否成功在指定路径上能否创建结点完成加锁。但其作为一个客户端通过一个threadlocal保存已获得锁的线程完成了锁的可重入,并且避免每次获取锁都创建一次结点,因为它是一个客户端,因此threadlocal在这里使用是本地的操作,从设计来讲,这样的实现,比将threadlocal放在zookeeper内每次传送当前线程的id完成锁的可重入会完美许多。
举个redis锁的实现:
有一个计算流量的逻辑,需要每隔一段时间将单机中的流量缓存同步到Redis库中,现在就出现了这个问题,一个服务被部署到多服务器上,当需要同时写同一台redis数据库,就需要使用分布式锁。
这里使用了Redis来实现分布式锁,实现的逻辑类似单机中使用的Lock或者synchronized,由于每个对象都拥有一个锁,只有获得了对象锁的线程才能对对象进行操作(在synchronized中使用了锁计数来完成,即通过判断是否非0来表示是否上锁)。
这里把锁对象变成锁KV对。
如果redis库中有特定的KV对,就表示上锁。
如果redis库中没有特定的KV对,就表示未上锁。
锁使用完毕后需要删除redis中的特定KV对。
实现代码:
/**
* 利用redis实现分布式锁
*
* @author zhenghao:
* @version 1.0 2016-6-7
*/
@Component
public class RedLock {
private static final Log LOG = LogFactory.getLog(RedLock.class);
private static final String REDLOCK_KEY = "RedLock";
private static final Long REDLOCK_EXPIRES = 5L;
private static final int LOCK_TIMES_LIMIT = 5;
private static volatile boolean locked = false;
private static String redLockValue = String.valueOf(Math.random());
private static AtomicInteger lockTimes = new AtomicInteger(0);
@Autowired
private CacheService cacheService;
/**
* 锁状态
*/
public boolean isLocked(){
return locked;
}
/**
* 加锁
* 通过Redis的setNx函数返回值判断加锁是否成功(1:成功, 0:失败)。
* 如若不成功,说明锁已被占用,休眠REDLOCK_EXPIRES后重新获取锁
* 加入对获取锁操作技术,超过一定范围说明锁竞争激烈,需要调整,否则会比较明显的降低程序的性能
*/
public void lock() {
while (cacheService.setRedLock(REDLOCK_KEY, redLockValue, REDLOCK_EXPIRES) == 0) {
try {
if (lockTimes.incrementAndGet() > LOCK_TIMES_LIMIT) {
LOG.info("SYSTEM_LOCK_CONTENTION: 锁竞争激烈");
}
Thread.sleep(REDLOCK_EXPIRES * 1000);
} catch (InterruptedException e) {
LOG.info("SYSTEM_INTERRUPTED_EXCEPTION: 内部异常中断错误");
}
}
locked = true;
}
/**
* 解除Redis锁
* 判断逻辑加入的原因在于:考虑一种情况,单机A获得了Redis锁,但A由于处理超时5秒后KEY超时,此条记录已被Redis移除,单机A被迫释放锁。
* 此时单机B进入已获得了Redis锁。
* 如若单机A中调用了Unlock逻辑,执行了removeRedLock操作,则会删除单机B正常获取到的锁。
* 判断单机B是否正常获取到了锁,通过比对保存的随机数完成。
* isLocked不置为false仅当A超时B拿到锁的情况下.
*/
public void unlock() {
String oldRedLockValue = cacheService.getRedLock(REDLOCK_KEY);
if (oldRedLockValue == null) {
locked = false;
} else if (oldRedLockValue.equals(redLockValue)) {
cacheService.removeRedLock(REDLOCK_KEY);
locked = false;
}
lockTimes.set(0);
}
}
update:
1.基于setnx函数实现的redis锁属于悲观锁,线程抢不抢得到锁只能看运气,在做了休眠操作可以减缓锁竞争的冲突,但当此应用横向扩展部署十多台以应对活动带来的高并发,线程用在在获取锁的时间将大大拉长,先不说是否会有线程一直拿不到锁而饿死,获取锁的时间将大大拉长导致平均响应时间的拉长,根据吞吐量的计算公式,系统的吞吐量将降低,原本横向扩展带来的吞吐量提升因为锁的争抢,提升效果将下降。
如果因为响应时间的拉长影响到了用户,想想我们平时上网网页开的慢或者半天打不开会干嘛,Ctrl+F5,更多的重复请求到达。更多的线程开启,更多的内存占用,更多的上下文切换时间,要么连接耗尽,要么系统拖死,挂了一台,剩下的就是雪崩效应。
2.使用消息队列,强行将原本的多线程变成单线程,再多线程监听消息。如果处理速度跟不上消费速度,内存会被吃掉,最后吃完。另外还要确保发送消息时不要出现诸如EOF等的错误,导致用户的请求并没有进入消息队列。
3.使用乐观锁,redis自带watch。消耗cpu。
具体场景使用具体的方法。