在单体的应用开发场景中涉及并发同步时,大家往往采用Synchronized(同步)或同一个JVM内Lock机制来解决多线程间的同步问题。而在分布式集群工作的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题,这种跨机器的锁就是分布式锁。
大致分为两种,
一种是数据库层面的锁,在一段读写期间,无其他线程侵入。
另一种向上提一级,是锁任务,将某一块代码逻辑锁住。锁去另一种机器拿取,如redis,zookeeper等等。
PS:理论上你可以自己写个单体的管理锁的服务,然后做好各种情况的响应。。。。
线程安全性(原子性、可见性、有序性)
一.单体应用:使用本地锁 + 数据库中的行锁解决
1.1 synchronized 同步锁范围要比事务的范围大。保证事件的原子性。避免事务还没提交,锁已释放。
public synchronized Long createOrder() throws Exception {
TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
Product product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product == null) {
platformTransactionManager.rollback(transaction1);
throw new Exception("购买商品:" + purchaseProductId + "不存在");
}
//商品当前库存
Integer currentCount = product.getCount();
//校验库存
if (purchaseProductNum > currentCount) {
platformTransactionManager.rollback(transaction1);
throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");
}
productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
// ... 省略 Set
return order.getId();
platformTransactionManager.commit(transaction1);
}
正确示例:使用Lock
private Lock lock = new ReentrantLock();
public Long createOrder() throws Exception{
Product product = null;
lock.lock();
TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
try {
product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){
throw new Exception("购买商品:"+purchaseProductId+"不存在");
}
//商品当前库存
Integer currentCount = product.getCount();
System.out.println(Thread.currentThread().getName()+"库存数:"+currentCount);
//校验库存
if (purchaseProductNum > currentCount){
throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
}
productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());
platformTransactionManager.commit(transaction1);
} catch (Exception e) {
platformTransactionManager.rollback(transaction1);
} finally {
// 注意抛异常的时候锁释放不掉,分布式锁也一样,都要在这里删掉
lock.unlock();
}
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
// ... 省略 Set
orderItemMapper.insertSelective(orderItem);
platformTransactionManager.commit(transaction);
return order.getId();
}
二.分布式应用:
1.使用数据库中的乐观锁,加一个 version 字段,利用CAS来实现。
2.使用数据库维护一张锁的表 + 悲观锁 select,使用 select for update 实现
3.使用Redis 的 setNX实现分布式锁
4.使用zookeeper的watcher + 有序临时节点来实现可阻塞的分布式锁
5.使用Redisson框架内的分布式锁来实现
6.使用curator 框架内的分布式锁来实现
分布式锁的真相与选择
6.1 分布式锁的真相
需要满足的几个特性
-
互斥:不同线程、进程互斥。
-
超时机制:临界区代码耗时导致,网络原因导致。可以使用额外的线程续命保证。
-
完备的锁接口:阻塞的和非阻塞的接口都要有,lock和tryLock。
-
可重入性:当前请求的节点+ 线程唯一标识。
-
公平性:锁唤醒时候,按照顺序唤醒。
-
正确性:进程内的锁不会因为报错死锁,因为崩溃的时候整个进程都会结束。但是多实例部署时死锁就很容易发生,如果粗暴使用超时机制解决死锁问题,就默认了下面这个假设:
-
锁的超时时间 >> 获取锁的时延 + 执行临界区代码的时间 + 各种进程的暂停(比如 GC)
-
但上述假设其实无法保证的。
-
将分布式锁定位为,可以容忍非常小概率互斥语义失效场景下的锁服务。一般来说,一个分布式锁服务,它的正确性要求越高,性能可能就会越低。
6.2 分布式锁的选择
-
数据库:db操作性能较差,并且有锁表的风险,一般不考虑。
-
优点:实现简单、易于理解
-
缺点:对数据库压力大
-
-
Redis:适用于并发量很大、性能要求很高而可靠性问题可以通过其他方案去弥补的场景。
-
优点:易于理解
-
缺点:自己实现、不支持阻塞
-
Redisson:相对于Jedis其实更多用在分布式的场景。
-
优点:提供锁的方法,可阻塞
-
-
-
Zookeeper:适用于高可靠(高可用),而并发量不是太高的场景。
-
优点:支持阻塞
-
缺点:需理解Zookeeper、程序复杂
-
-
Curator
-
优点:提供锁的方法
-
缺点:Zookeeper,强一致,慢
-
-
Etcd:安全和可靠性上有保证,但是比较重。
不推荐自己编写的分布式锁,推荐使用Redisson和Curator实现的分布式锁。