什么是超卖?
某件商品库存数量10件,结果卖出了15件。
会有什么影响?
- 商品卖出数量超出了库存数量
- 导致商家没有商品发货、发货时间延长、买卖双方容易发声纠纷
超卖现象一
系统中库存为1,A和B同事看到这个商品,加入购物车,并同时提交订单,所以产生了两笔订单,
卖家在商品发货时,发现只有1件商品,但是有两笔订单,导致卖家无法给其中一方发货。
产生原因:
- 扣减库存的动作,在程序中进行 在程序中由于并发原因,导致库存计算错误

超卖现象二
并发校验库存时,造成库存充足的假象,update更新库存导致系统中库存为负数

为什么要使用分布式锁
在开发应用过程中,如果需要对一个共享变量(上述超卖现象的库存)进行多线程同步访问,
虽然可以使用我们Synchronized、ReentrantLock进行处理,但随业务发展,需要做集群时,
一个应用需要部署到多台服务器上,这时候我们就需要借助第三方组件来控制共享变量,即分布式锁。
分布式锁应该具备哪些条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 高可用、高性能的获取锁与释放锁
- 具备可重入特性
- 具备锁失效机制,防止死锁
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
分布式锁实现方式
第一种:基于数据库悲观锁的分布式锁
多个进程、多个线程访问共同组件数据库。通过select……for update访问同一条数据,
利用数据库for update特性锁住数据,使得其他线程只能等待,从而达到分布式锁的效果。
第二种:基于Redis的Setnx实现分布式锁
- 获取锁时,使用setnx进行加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
- 获取锁的时候还设置一个获取的超时时间,若超过这个时间放弃获取锁。
- 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
- 经历上述操作后,因为释放锁的方法不是原子操作导致的,那么我们只要保证释放锁的代码是原子性的就能解决该问题了。(引入LUA脚本)
String key = "redisKey"; String value = UUID.randomUUID().toString(); RedisCallback<Boolean> redisCallback = connection -> { //设置NX RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent(); //设置过期时间 Expiration expiration = Expiration.second(30); //序列化key byte[] redisKey = redisTemplate.getKeySerializer().serialize(key); //序列化value byte[] redisValue = redisTemplate.getValueSerializer().serialize(value); Boolean result = connection.set(redisKey,redisValue,expiration,); } //获取分布式锁 Boolean lock = (Boolean) redisTemplate.excute(redisCallback,setOption ) if(lock){ log.info("我进入了锁"); try { Thread.sleep(15000); } catch (InterruptedException e) { e.printStackTrace(); } finally { RedisScript<Boolean> redisScript = RedisScirpt.of("if redis.call("get",KEYS[1] ) == ARGV[1] then return redis.call("del",KEY[1]) else return 0 end",Boolean.class); List<String> keys = Arrays.asList(key); Object result = (Boolean) redisTemple.excute(redisScript,keys,value); } }
第三种:使用Zookeeper的实现方式
利用Zookeeper的瞬时有序节点的特性;当多线程并发创建瞬时节点时,会得到有序的序列;
而序号最小的线程获得锁;其他未获取锁的线程则监听自己序号的前一个序号;
当前一个线程执行完成,删除自己序号的节点;下一个序号的线程得到通知,继续执行。原理图解为:

第四种:基于Zookeper的Curator的客户端实现分布式锁
package com.xbq.zookeeper.curator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; 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.RetryNTimes; /** * 使用Curator来实现分布式锁 * @author xbq */ public class LockByCurator { // 此demo使用的集群,所以有多个ip和端口 private static String CONNECT_SERVER = "192.168.242.129:2181,192.168.242.129:2182,192.168.242.129:2183"; // session过期时间 private static int SESSION_TIMEOUT = 3000; // 连接超时时间 private static int CONNECTION_TIMEOUT = 3000; // 锁节点 private static final String CURATOR_LOCK = "/curatorLock"; /** * 获取锁操作 * @param cf */ public static void doLock(CuratorFramework cf){ System.out.println(Thread.currentThread().getName() + " 尝试获取锁!"); // 实例化 zk分布式锁 InterProcessMutex mutex = new InterProcessMutex(cf, CURATOR_LOCK); try { // 判断是否获取到了zk分布式锁 if(mutex.acquire(5, TimeUnit.SECONDS)){ System.out.println(Thread.currentThread().getName() + " 获取到了锁!-------"); // 业务操作 Thread.sleep(5000); } } catch (Exception e) { e.printStackTrace(); } finally { try { // 释放锁 mutex.release(); } catch (Exception e) { e.printStackTrace(); } } } /** * 测试 * @param args */ public static void main(String[] args) { // 定义线程池 ExecutorService service = Executors.newCachedThreadPool(); // 定义信号灯,只能允许10个线程并发操作 final Semaphore semaphore = new Semaphore(10); // 模拟10个客户端 for(int i=0; i < 10 ;i++){ Runnable runnable = new Runnable() { @Override public void run() { try { semaphore.acquire(); // 连接 ZooKeeper CuratorFramework framework = CuratorFrameworkFactory. newClient(CONNECT_SERVER, SESSION_TIMEOUT, CONNECTION_TIMEOUT, new RetryNTimes(10,5000)); // 启动 framework.start(); doLock(framework); semaphore.release(); } catch (Exception e) { } } }; service.execute(runnable); } service.shutdown(); } }
第五种:基于Redisson实现分布式锁
添加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
注入RedissonConfig Bean
@Configuration public class RedissonConfig { @Bean public RedissonClient getClient(){ Config config = new Config(); //设置redis config.useSingleServer().setAddress("redis://127.0.0.1:6379"); //初始化Redission RedissonClient redisson = Redisson.create(config); return redisson; } }
实现分布式锁
//获取分布式锁 RLock lock = redisson.getLock("lockName"); try{ // 1. 最常见的使用方法 //lock.lock(); // 2. 支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁 //lock.lock(10, TimeUnit.SECONDS); // 3. 尝试加锁,最多等待2秒,上锁以后8秒自动解锁 boolean res = lock.tryLock(2, 8, TimeUnit.SECONDS); if(res){ //成功 //处理业务 } } catch (InterruptedException e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); }
分布式锁实现方案优缺点分析

总结:不推荐自己编写分布式锁,推荐使用Redisson和Curator实现的分布式锁(经历大量验证)
浙公网安备 33010602011771号