使用Redis分布式锁处理并发,解决超卖问题

本文探讨了使用Apacheab进行并发压测的方法,并分析了synchronized处理并发问题的局限性。介绍了Redis分布式锁的实现,包括使用SETNX和GETSET命令,以及如何避免死锁。通过实际案例展示了如何在下单代码中应用Redis锁,确保并发安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、使用Apache ab模拟并发压测

1、压测工具介绍

$ ab -n 100 -c 100 http://www.baidu.com/

-n表示发出100个请求,-c模拟100个并发,相当是100个人同时访问。

还可以这样写:

$ ab -t 60 -c 100 http://www.baidu.com/

-t表示60秒,-c是100个并发,会在连续60秒内不停的发出请求。

使用ab工具模拟多线程并发请求,对发出负载的机器要求比较低,既不会占用很多cpu,也不会占用很多的内存,因此也是很多DDoS攻击的必备良药,不过要慎用,别耗光自己机器的资源。通常来说1000个请求,100个并发算是比较正常的模拟。

至于工具的使用,具体见:Apache ab 测试工具使用(一)

下载后,进入support文件夹,执行命令。

2、并发测试

我创建了两张表,一个商品表,一个订单记录表;
然后写了两个接口,一个是查询商品信息,一个是下单秒杀。

查询订单:

image_1caapork5q212c91aschplhfop.png-48.5kB

秒杀下单:

image_1caapplln4pqcnk1q4fmv44vj16.png-45.3kB

当我并发测试时:

$ ab -n 500 -c 100 http://localhost:8080/seckill/1/

image_1cabf0e35c361t5qltaqj03au13.png-65.1kB

这TM肯定不行啊,这就超卖了,明明没这么多商品,结果还卖出去了。。。

二、synchronized处理并发

首先,synchronized的确是一个解决办法,而且也很简单,在方法前面加一个synchronized关键字。

但是通过压测,发现请求变的很慢,因为:
synchronized就用一个锁把这个方法锁住了,每次访问这个方法,只会有一个线程,所以这就是它导致慢的原因。通过这种方式,保证这个方法中的代码都是单线程来处理,不会出什么问题。

同时,使用synchronized还是存在一些问题的,首先,它无法做到细粒度的控制,比如同一时间有秒杀A商品和B商品的请求,都进入到了这个方法,虽然秒杀A商品的人很多,但是秒杀B商品的人很少,但是即使是买B商品,进入到了这个方法,也会一样的慢。

最重要的是,它只适合单点的情况。如果以后程序水平扩展了,弄了个集群,很显然,负载均衡之后,不同的用户看到的结果一定是五花八门的。

所以,还是使用更好的办法,使用redis分布式锁。

三、redis分布式锁

1、两个redis的命令

setnx key value 简单来说,setnx就是,如果没有这个key,那么就set一个key-value, 但是如果这个key已经存在,那么将不会再次设置,get出来的value还是最开始set进去的那个value.
网站中还专门讲到可以使用!SETNX加锁,如果获得锁,返回1,如果返回0,那么该键已经被其他的客户端锁定。
并且也提到了如何处理死锁。

getset key value 这个就更简单了,先通过key获取value,然后再将新的value set进去。

2、redis分布式锁的实现

我们希望的,无非就是这一段代码,能够单线程的去访问,因此在这段代码之前给他加锁,相应的,这段代码后面要给它解锁:

image_1cabec77q16dibn41a207mkpb19.png-80.3kB

2.1 引入redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2 配置redis

spring:
  redis:
    host: localhost
    port: 6379

2.3 编写加锁和解锁的方法

package com.vito.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * Created by VitoYi on 2018/4/5.
 */
@Component
public class RedisLock {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key   商品id
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key, String value) {
        if (redisTemplate.opsForValue().setIfAbsent(key, value)) {     //这个其实就是setnx命令,只不过在java这边稍有变化,返回的是boolea
            return true;
        }

        //避免死锁,且只让一个线程拿到锁
        String currentValue = redisTemplate.opsForValue().get(key);
        //如果锁过期了
        if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间
            String oldValues = redisTemplate.opsForValue().getAndSet(key, value);

            /*
               只会让一个线程拿到锁
               如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了
             */
            if (!StringUtils.isEmpty(oldValues) && oldValues.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }


    /**
     * 解锁
     * @param key
     * @param value
     */
    public void unlock(String key, String value) {
        try {
            String currentValue = redisTemplate.opsForValue().get(key);
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
            logger.error("『redis分布式锁』解锁异常,{}", e);
        }
    }
}

为什么要有避免死锁的一步呢?
假设没有『避免死锁』这一步,结果在执行到下单代码的时候出了问题,毕竟操作数据库、网络、io的时候抛了个异常,这个异常是偶然抛出来的,就那么偶尔一次,那么会导致解锁步骤不去执行,这时候就没有解锁,后面的请求进来自然也或得不到锁,这就被称之为死锁。
而这里的『避免死锁』,就是给锁加了一个过期时间,如果锁超时了,就返回true,解开之前的那个死锁。

2.4 下单代码中引入加锁和解锁,确保只有一个线程操作

@Autowired
private RedisLock redisLock;

@Override
@Transactional
public String seckill(Integer id)throws RuntimeException {
    //加锁
    long time = System.currentTimeMillis() + 1000*10;  //超时时间:10秒,最好设为常量

    boolean isLock = redisLock.lock(String.valueOf(id), String.valueOf(time));
    if(!isLock){
        throw new RuntimeException("人太多了,换个姿势再试试~");
    }

    //查库存
    Product product = productMapper.findById(id);
    if(product.getStock()==0) throw new RuntimeException("已经卖光");
    //写入订单表
    Order order=new Order();
    order.setProductId(product.getId());
    order.setProductName(product.getName());
    orderMapper.add(order);
    //减库存
    product.setPrice(null);
    product.setName(null);
    product.setStock(product.getStock()-1);
    productMapper.update(product);

    //解锁
    redisLock.unlock(String.valueOf(id),String.valueOf(time));

    return findProductInfo(id);
}

这样再来跑几次压测,就不会超卖了:

image_1cabeppmqfn11gau8gu4gn6a5m.png-56.2kB

<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{安全性公式}:持有时间 < 时时间 < 业务最大容忍时间 $$
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值