一、什么是超卖
- 举例:某件商品库存数量10件,结果卖出了15件
二、超卖现象一
-
系统中库存为1,但是产生了两笔订单
-
解决方法
- 扣减库存不在程序中进行,而是通过数据库
- 向数据库传递库存增量,扣减1个库存,增量为-1
先 - 在数据库update语句计算库存,通过update行锁解决并发
<update id="updateProductCount">
update product
set count = count - #{purchaseProductNum,jdbcType=INTEGER},
update_user = #{updateUser,jdbcType=VARCHAR},
update_time = #{updateTime,jdbcType=TIME}
where id = #{id,jdbcType=INTEGER}
</update>
三、超卖现象二
- 系统中库存变为 -1
- 产生原因:并发检验库存,造成库存充足的假象,update更新库存,导致库存为负数
- 解决方法:校验库存、扣减库存统一加锁,使之成为原子性的操作,并发时只有获得锁的线程才能校验、扣减库存
使用synchronized锁,注意(事务要在锁中)
使用ReentrantLock锁解决(并发包中的锁)
private Lock lock = new ReentrantLock();
public Integer createOrder() throws Exception{
Product product = null;
lock.lock();
try {
TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){
platformTransactionManager.rollback(transaction1);
throw new Exception("购买商品:"+purchaseProductId+"不存在");
}
//商品当前库存
Integer currentCount = product.getCount();
System.out.println(Thread.currentThread().getName()+"库存数:"+currentCount);
//校验库存
if (purchaseProductNum > currentCount){
platformTransactionManager.rollback(transaction1);
throw
new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
}
productMapper.updateProductCount(purchaseProductNum,"xxx",new Date(),product.getId());
platformTransactionManager.commit(transaction1);
}finally {
lock.unlock();
}
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
Order order = new Order();
order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
order.setOrderStatus(1);//待处理
order.setReceiverName("xxx");
order.setReceiverMobile("13311112222");
order.setCreateTime(new Date());
order.setCreateUser("xxx");
order.setUpdateTime(new Date());
order.setUpdateUser("xxx");
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
orderItem.setProductId(product.getId());
orderItem.setPurchasePrice(product.getPrice());
orderItem.setPurchaseNum(purchaseProductNum);
orderItem.setCreateUser("xxx");
orderItem.setCreateTime(new Date());
orderItem.setUpdateTime(new Date());
orderItem.setUpdateUser("xxx");
orderItemMapper.insertSelective(orderItem);
platformTransactionManager.commit(transaction);
return order.getId();
}
单体应用锁的局限性不能跨jvm,所以需要使用分布式锁
四、基于数据库悲观锁的分布式锁
基于数据库实现分布式锁(跨进程)
步骤:
- 多个进程、多个线程访问共同组件数据库
- 通过select…for update 访问同一条数据
- for update锁定数据,其他线程只能等待
优点:
简单方便、易于理解、易于操作
缺点:
并发量大时,对数据库压力较大
select * from distribute_lock where business_code='demo' for update
@RestController
@Slf4j
public class DemoController {
@Resource
private DistributeLockMapper distributeLockMapper;
@RequestMapping("singleLock")
@Transactional(rollbackFor = Exception.class)
public String singleLock() throws Exception {
log.info("我进入了方法!");
DistributeLock distributeLock = distributeLockMapper.selectDistributeLock("demo");
if (distributeLock==null) throw new Exception("分布式锁找不到");
log.info("我进入了锁!");
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "我已经执行完成!";
}
}
五、基于Redis的Setnx实现分布式锁
- 实现原理:
- 获取锁的Redis命令
- SET resource_name my_random_value NX PX 30000
解释:
resource_name :资源名称,可根据不同的业务区分不同的锁
my_random_value :随机值,每个线程的随机值都不同,用于释放时的校验
NX:key不存在时设置成功,key存在则设置不成功
PX:自动失效时间,出现异常情况,锁可以过期失效 - 利用NX的原子性,多个线程并发时,只有一个线程可以设置成功
- 设置成功即获得锁,可以执行后续的业务
- 如果出现异常,过了锁的有效期,锁自动释放
- 释放锁采用Redis的delete命令
- 释放锁时校验之前设置的随机数,相同才能释放
- 释放锁的LUA脚本
自己的理解:使用Redis的Setnx这个特性,给一个key赋值,key存在择赋值不成功,key不存在赋值成功
LUA脚本
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
redis实现分布式锁代码
RedisLock.java
@Slf4j
public class RedisLock implements AutoCloseable {
private RedisTemplate redisTemplate;
private String key;
private String value;
//单位:秒
private int expireTime;
public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime=expireTime;
this.value = UUID.randomUUID().toString();
}
/**
* 获取分布式锁
* @return
*/
public boolean getLock(){
RedisCallback<Boolean> redisCallback = connection -> {
//设置NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//设置过期时间
Expiration expiration = Expiration.seconds(expireTime);
//序列化key
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
//序列化value
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
//执行setnx操作
Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
return result;
};
//获取分布式锁
Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
return lock;
}
public boolean unLock() {
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
List<String> keys = Arrays.asList(key);
Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
log.info("释放锁的结果:"+result);
return result;
}
@Override
public void close() throws Exception {
unLock();
}
}
RedisLockController.java
@RestController
@Slf4j
public class RedisLockController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("redisLock")
public String redisLock(){
log.info("我进入了方法!");
try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
if (redisLock.getLock()) {
log.info("我进入了锁!!");
Thread.sleep(15000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
log.info("方法执行完成");
return "方法执行完成";
}
}
六、基于Zookeeper的瞬间节点实现分布式锁
- Zookeeper的数据结构
是一棵树,持久节点:zk会话结束和zk重启这些会话都是不会消失的,瞬时节点:瞬时节点不可再有子节点,会话结束后瞬时节点自动消失 - Zookeeper的观察器
可设置观察器的3个方法:getData();getChildren();exists()
节点数据发生变化,发送给客户端
观察器只能控制一次,再监控需重新设置 - 实现原理:
利用Zookeeper的瞬时有序节点的特性
多线程并发创建瞬时节点时,得到有序的序列
序号最小的线程获得锁
其他的线程则监听自己序号的前一个序号
前一个线程执行完成,删除自己序号的节点
下一个序号的线程得到通知,继续执行
创建节点时,已经确定了线程的执行顺序
ZkLock.java
@Slf4j
public class ZkLock implements AutoCloseable, Watcher {
private ZooKeeper zooKeeper;
private String znode;
public ZkLock() throws IOException {
this.zooKeeper = new ZooKeeper("localhost:2181",
10000,this);
}
public boolean getLock(String businessCode) {
try {
//创建业务 根节点
Stat stat = zooKeeper.exists("/" + businessCode, false);
if (stat==null){
zooKeeper.create("/" + businessCode,businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
//创建瞬时有序节点 /order/order_00000001
znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
//获取业务节点下 所有的子节点
List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
//子节点排序
Collections.sort(childrenNodes);
//获取序号最小的(第一个)子节点
String firstNode = childrenNodes.get(0);
//如果创建的节点是第一个子节点,则获得锁
if (znode.endsWith(firstNode)){
return true;
}
//不是第一个子节点,则监听前一个节点
String lastNode = firstNode;
for (String node:childrenNodes){
if (znode.endsWith(node)){
zooKeeper.exists("/"+businessCode+"/"+lastNode,true);
break;
}else {
lastNode = node;
}
}
synchronized (this){
wait();
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Override
public void close() throws Exception {
zooKeeper.delete(znode,-1);
zooKeeper.close();
log.info("我已经释放了锁!");
}
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted){
synchronized (this){
notify();
}
}
}
}
七、基于分布式Zookeeper的Curator客户端实现分布式锁
- curator已经实现了分布式锁的方法
- 直接调用即可
@Test
public void testCuratorLock(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/order");
try {
if ( lock.acquire(30, TimeUnit.SECONDS) ) {
try {
log.info("我获得了锁!!!");
}
finally {
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}
client.close();
}
@RestController
@Slf4j
public class ZookeeperController {
@Autowired
private CuratorFramework client;
@RequestMapping("zkLock")
public String zookeeperLock(){
log.info("我进入了方法!");
try (ZkLock zkLock = new ZkLock()) {
if (zkLock.getLock("order")){
log.info("我获得了锁");
Thread.sleep(10000);
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
log.info("方法执行完成!");
return "方法执行完成!";
}
@RequestMapping("curatorLock")
public String curatorLock(){
log.info("我进入了方法!");
InterProcessMutex lock = new InterProcessMutex(client, "/order");
try{
if (lock.acquire(30, TimeUnit.SECONDS)){
log.info("我获得了锁!!");
Thread.sleep(10000);
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
log.info("我释放了锁!!");
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
log.info("方法执行完成!");
return "方法执行完成!";
}
}
八、基于Redisson实现分布式锁