什么是超卖?
某件商品库存数量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号