分布式锁解决超卖问题

在单体应用场景下处理多线程并发问题时,我们常会用到Synchronized和Lock锁。而在分布式场景中,则需要一种更高级的锁机制来处理跨机器的进程之间的数据同步问题,这就是分布式锁。

1. 商品超卖场景

分布式锁解决的最典型问题就是商品超卖问题了,即商品库存为1,但是由于并发下单,导致产生了多笔订单。

2. 单体应用下的解决方法

使用事务注解的坑

由于创建商品订单涉及扣减库存、创建订单两个操作,所以需要用到事务。
但是如果采用事务注解@Transactional,事务提交是在方法结束的时候执行,此时方法也会释放锁,导致并发的下一个线程会与事务提交并行执行,也会导致库存扣减异常。

2.1. Synchronized + 编程式事务


public Long createOrder() throws Exception {
    Product product = null;
    //synchronized (this) {
    //synchronized (object) {
    synchronized (DBOrderService2.class) {
        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, new Date(), product.getId());
        platformTransactionManager.commit(transaction1);
    }

    TransactionStatus transaction2 = platformTransactionManager.getTransaction(transactionDefinition);

    Order order = new Order();
    // ... 省略 Set
    orderMapper.insertSelective(order);

    OrderItem orderItem = new OrderItem();
    // ... 省略 Set
    orderItemMapper.insertSelective(orderItem);
    platformTransactionManager.commit(transaction2);
    return order.getId();
}

2.2. 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(); // lock锁的释放需要放在finally中,确保异常情况下也能成功释放锁
    }

    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();
}

3. 分布式锁

当项目采用集群分布式部署,单机锁就会失效,此时需要采用分布式锁解决该问题。
常见的分布式锁的实现方式有如下几种:

3.1. 基于Innodb引擎的数据库

数据库实现分布式锁分两种:

3.1.1. select … for update

在这里插入图片描述

使用数据库表唯一键作为限制,向表中插入一条数据,抢锁的时候,使用select for update查询锁对应的key,如果查询到了,代表抢占锁成功,会给数据上表锁,此时其他线程的SQL执行会被阻塞。当这条数据被删除后,锁被释放。

// 加上事务就是为了 for update 的锁可以一直生效到事务执行结束
@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(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "我已经执行完成!";
}

<select id="selectDistributeLock" resultType="com.deltaqin.distribute.model.DistributeLock">
  select * from distribute_lock
  where businessCode = #{businessCode,jdbcType=VARCHAR}
  for update
</select>
3.1.2. insert lock

即维护一张锁表,插入数据代表获取锁,删除数据代表释放锁

@Autowired
private MethodlockMapper methodlockMapper;

@Override
public boolean tryLock() {
    try {
        //插入一条数据   insert into
        methodlockMapper.insert(new Methodlock("lock"));
    }catch (Exception e){
        //插入失败
        return false;
    }
    return true;
}

@Override
public void waitLock() {
    try {
        Thread.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

@Override
public void unlock() {
    //删除数据   delete
    methodlockMapper.deleteByMethodlock("lock");
    System.out.println("-------释放锁------");
}

3.2. 基于Redis的setnx

获取锁的命令

SET resource_name my_random_value NX PX 30000
  • source_name:资源名称,可根据不同的业务区分不同的锁
  • my_random_value:随机值,每个线程的随机值都不同,用于释放锁时的校验
  • NX:key不存在的时候设置成功,key存在的时候设置不成功(redis执行命令是单线程的,所以命令的执行是原子操作)
  • PX:自动失效时间(防止程序异常导致没释放锁),过期后锁会失效

获取锁的实现步骤

利用NX的原子性,多个线程并发时,只有一个线程可以设置成功
设置成功即获得锁,执行后续的业务处理
如果出现异常,过了锁的有效期,锁自动释放

释放锁的步骤

释放锁时校验之前设置的随机数,相同才释放(保证释放的是自己的锁)
释放锁采用LUA脚本(因为redis的delete命令不支持删除的时候校验值)

释放锁的LUA脚本

if redis.call("get", KEYS[1]) == ARGV[1] then
  return redis.call("del", KEYS[1])
else
  return 0
end

封装基于redis的setnx实现的分布式锁对象 RedisLock

/**
 * 封装redis分布式锁
 * 实现AutoCloseable接口,重写close()方法,可以添加finally或关闭流的操作
 * @author kyrielx
 * @since 2023/2/6
 */
@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.value = UUID.randomUUID().toString();
        this.expireTime = expireTime;
    }

    public boolean getLock(){
        RedisCallback<Boolean> redisCallback = redisConnection -> {
            // 设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            // 设置EX
            Expiration expiration = Expiration.seconds(30);
            // 序列化key
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            // 序列化value
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            // 执行setnx操作
            Boolean result = redisConnection.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;
    }

    // 关闭的操作(redis中写入的数据过期时会自动调用此方法,JDK1.7之后支持自动关闭)
    @Override
    public void close() throws Exception {
        unLock();
    }
}

在Springboot项目中使用RedisLock

  1. 先引入starter依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 在配置文件中,添加相关配置
spring.redis.host=localhost
  1. 使用(每次获取锁的时候,自己线程需要new一个对应的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 "方法执行完成";
}

3.3. 基于zk的临时节点+watcher监听机制

zk的临时节点会在客户端与zk连接的会话断开时自动删除
zk的临时节点不能有子节点
zk的临时节点创建后会得到有序的序列,每个节点都会有一个序号
zk的watcher机制只能监听一次,如果需要继续监听,可以自行设置添加watcher

基于zk的临时顺序节点实现分布式锁的原理

  1. 多线程并发创建瞬时节点的时候,得到有序的序列,序号最小的线程可以获得锁;
  2. 其他的线程监听自己序号的前一个序号。前一个线程执行结束之后删除自己序号的节点;
  3. 下一个序号的线程得到通知,继续执行;
  4. 以此类推,创建节点的时候,就确认了线程执行的顺序。

实现代码


/**
 * 如果创建的节点是第一个节点,就获得锁;否则监听自己的前序节点
 * 自己本身就是一个watcher,可以得到通知
 * AutoCloseable 资源不使用的时候,实现自动关闭
 */
@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();
            }
        }
    }
}

3.4. 基于curator客户端

  1. 引入curator客户端依赖
<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-recipes</artifactId>
  <version>4.2.0</version>
</dependency>
  1. 在启动类中创建bean
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean(initMethod = "start", destroyMethod = "close")
    public CuratorFramework getCuratorFramework(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
        return client;
    }
}
  1. 直接使用其实现的分布式锁
@RestController
@Slf4j
public class ZkLockController {

    @Autowired
    private CuratorFramework client;

    /**
     * 使用基于zookeeper的curator客户端,实现分布式锁
     */
    @GetMapping("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 (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                lock.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        log.info("方法执行完成!");
        return "方法执行完成!";
    }

}

3.5. 基于redisson

Spring项目中使用

  1. 引入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.19.3</version>
</dependency>
  1. 测试用例
@Test
public void RedissonLockTest(){
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    RedissonClient redissonClient = Redisson.create(config);
    RLock rLock = redissonClient.getLock("order");
    log.info("我进入了方法!");
    try {
        rLock.lock(30, TimeUnit.SECONDS);
        log.info("我获得了锁!");
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        log.info("我释放了锁!");
        rLock.unlock();
    }
    log.info("方法执行完成!");
}

Springboot中使用

相当于通过引入redisson starter,简化了redissionClient的初始化过程。

  1. 引入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.11.2</version>
</dependency>
  1. 添加配置
# 单节点
redisson:
  single_server_config:
    password: null
    address: "redis://127.0.0.1:6379"


# 集群
redisson:
  sentinel-servers-config:
    master-name: "mymaster"
    sentinel-address:
      - "redis://192.168.2.170:26377"
      - "redis://192.168.2.170:26378"
      - "redis://192.168.2.170:26379"
    password: bxkc2016
  1. 使用
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedissonSpringBootStarterApplicationTests {
    
    @Autowired
    private RedissonClient redissonClient;

    @Test
    public void start() {
        RLock rLock = redissonClient.getLock("order");
        log.info("我进入了方法!");
        try {
            rLock.lock(30, TimeUnit.SECONDS);
            log.info("我获得了锁!");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.info("我释放了锁!");
            rLock.unlock();
        }
        log.info("方法执行完成!");
    }
}

4. 总结

对于几种分布式锁的实现方案,进行优缺点的分析
在这里插入图片描述

推荐使用Redisson和Curator上实现的分布式锁
不推荐自己编码实现分布式锁

5. 参考资料

分布式锁实现原理与最佳实践 - 阿里云开发者(微信公众号)

<think>嗯,用户想了解如何使用Redis实现分布式锁解决商品问题。首先,我得回忆一下Redis分布式锁的基本概念和相关命令。记得SETNX和EXPIRE是关键,不过现在可能更推荐用SET命令结合NX和PX选项,这样可以原子性地设置键和过期时间,避免客户端崩溃导致锁无法释放的情况。 然后,用户提到商品,这通常发生在高并发下,多个请求同时检查库存,都认为还有库存,然后同时扣减,导致实际库存不足。所以需要分布式锁来保证同一时间只有一个请求可以处理库存扣减。 接下来,我需要考虑如何设计这个锁的获取和释放流程。获取锁的时候,要生成一个唯一的value,比如UUID,这样在释放锁的时候可以验证是否是自己持有的锁,防止误删其他客户端的锁。释放锁的时候,需要原子性地执行判断和删除操作,可以用Lua脚本来实现,因为Lua脚本在Redis中是原子执行的。 另外,锁的过期时间设置也很重要。时间不能太短,否则业务还没执行完锁就自动释放了,会导致其他客户端获取锁,同样出现。也不能太长,否则系统故障时恢复时间变长。可能需要根据业务处理的时间来评估,或者使用看门狗机制自动续期,但这样会增加复杂性。可能用户需要一个简单可靠的方案,所以暂时先设置合理的固定过期时间。 然后,用户可能需要一个具体的代码示例,比如用Java或者Python。不过用户之前给出的示例是Python,所以可能倾向于Python的代码。不过要注意连接Redis的库,比如使用redis-py,以及如何实现原子操作。 还要考虑异常情况,比如在扣减库存后,确保释放锁,避免死锁。所以需要try-finally块来保证锁的释放。此外,网络延迟或Redis故障的情况,可能需要重试机制,或者使用RedLock算法来跨多个Redis实例获取锁,提高容错性。但RedLock实现起来更复杂,用户可能只需要单实例的基础方案,所以先提供基础方案,再提到高级方案作为优化方向。 最后,需要总结最佳实践,包括原子性操作、唯一标识、合理时、容错处理,以及可能的扩展方案如RedLock。同时,生成相关问题,帮助用户进一步学习,比如锁失效的情况、RedLock的实现、与其他分布式锁的比较,以及库存扣减的其他方案如预扣库存。</think>### 如何使用Redis实现分布式锁解决商品问题 #### 1. 问题背景 商品指在高并发场景下,库存数量被多次扣减导致实际库存为负数。例如:商品库存仅剩1件时,多个用户同时请求购买,若未加锁可能全部成功下单[^1]。 #### 2. 实现原理 使用Redis的`SET key value NX PX timeout`命令实现分布式锁: - `NX`:仅当键不存在时设置值 - `PX`:设置键的过期时间(毫秒) - 唯一标识(如UUID)确保锁归属 - 原子性操作避免锁失效问题[^2] #### 3. 具体实现步骤 **3.1 获取锁** ```python import redis import uuid redis_client = redis.StrictRedis() def acquire_lock(lock_key, timeout=3000): identifier = str(uuid.uuid4()) # 原子性设置锁(NX + PX) success = redis_client.set(lock_key, identifier, nx=True, px=timeout) return identifier if success else None ``` **3.2 释放锁(Lua脚本保证原子性)** ```lua if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end ``` ```python def release_lock(lock_key, identifier): script = """ if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end""" return redis_client.eval(script, 1, lock_key, identifier) ``` **3.3 库存扣减流程** ```python def deduct_stock(product_id): lock_key = f"lock:{product_id}" identifier = acquire_lock(lock_key) if not identifier: return "系统繁忙,请重试" try: # 查询库存 stock = redis_client.get(f"stock:{product_id}") if int(stock) <= 0: return "库存不足" # 扣减库存 redis_client.decr(f"stock:{product_id}") return "购买成功" finally: release_lock(lock_key, identifier) ``` #### 4. 最佳实践 1. **原子性操作**:使用`SET NX PX`而非分开执行`SETNX`和`EXPIRE`[^2] 2. **唯一标识**:防止误删其他客户端的锁 3. **合理时**:根据业务复杂度设置3000-10000ms 4. **容错处理**:配合Redis Sentinel/Cluster实现高可用[^3] 5. **重试机制**:获取锁失败后添加随机退避重试 #### 5. 扩展优化方案 - **RedLock算法**:跨多个独立Redis实例实现强一致性锁 - **自动续期**:通过守护线程延长锁有效期(需处理复杂性) - **库存预扣**:结合消息队列实现异步库存处理 $$ \text{锁安全性公式}:锁持有时间 < 锁时时间 < 业务最大容忍时间 $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kyrielx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值