【redis】redis分布式锁实现商品秒杀功能

在多人访问网站时,如果不加锁,就会出现并发问题。下面我们先来测试进行模拟商品秒杀的场景:

首先我们编写两个方法一个用于下单减去库存,一个用于查询商品库存:

@Service
public class SecKillServiceImpl implements SecKillService {

    /**
     * 中秋活动 秒杀月饼 限量100000
     */
    static Map<String, Integer> products;
    static Map<String, Integer> stock;
    static Map<String, String> orders;

    static {
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        products.put("1001", 100000);
        stock.put("1001", 100000);
    }

    private String queryMap(String productId) {
        return "中秋活动,月饼特价,限量份"
                + products.get(productId)
                + " 还剩:" + stock.get(productId) + " 份"
                + " 该商品成功下单用户数目:"
                + orders.size() + " 人";
    }


    @Override
    public String querySecKillProductInfo(String productId) {
        return queryMap(productId);
    }


    @Override
    public  void orderProductMockDiffUser(String productId) {
        int stockNum = stock.get(productId);
        if (stockNum == 0) {
            //库存不足
            throw new SellException(100, "活动已经结束,请留意下次活动");
        } else {

            orders.put(KeyUtils.getUniqueKey(), productId);
            stockNum = stockNum - 1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            stock.put(productId, stockNum);
        }
    }
}

编写Controller层进行测试

@RestController
@RequestMapping("/kill")
@Slf4j
public class SecKillController {
    @Autowired
    private SecKillService secKillService;

    /**
     * 查询秒杀活动特价商品的信息
     * @param productId
     * @return
     */
    @GetMapping("/query/{productId}")
    public String query(@PathVariable String productId)throws Exception{
        return secKillService.querySecKillProductInfo(productId);
    }


    /**
     * 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
     * @param productId
     * @return
     * @throws Exception
     */
    @GetMapping("/order/{productId}")
    public String skill(@PathVariable String productId)throws Exception{
        log.info("@skill request, productId:" + productId);
        secKillService.orderProductMockDiffUser(productId);
        return secKillService.querySecKillProductInfo(productId);
    }
}

一.我们先来测试以下不加锁的情况:

1.首先我们测试正常情况:下单三次,总量= 剩余+下单数量  

2.使用压测工具apache ab进行模拟1000人下单的情况

输入命令:

ab -n 1000 -c 10 http://127.0.0.1:8080/kill/order/1001

接下来我们查询结果:

可以看到这时候的数量明显不对: 下单1003次,总量 < 剩余 + 下单数

3.这时我们使用传统的synchronized锁将整个方法锁起来继续测试:

//synchronized处理锁
@Override
public synchronized void orderProductMockDiffUser(String productId) {
    int stockNum = stock.get(productId);
    if (stockNum == 0) {
        //库存不足
        throw new SellException(100, "活动已经结束,请留意下次活动");
    } else {

        orders.put(KeyUtils.getUniqueKey(), productId);
        stockNum = stockNum - 1;
        try {
            Thread.sleep(100);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        stock.put(productId, stockNum);
    }
}

这次我们同样模拟1000人下单:

可以明显看得到,请求速度非常得慢,虽所结果对的上,但系统将每个请求处理一遍,这样对服务器的性能要求就非常高。

4.接下来我们使用redis的分布式锁来进行测试

我们可以在进入下单的方法后将核心的方法加锁,然后离开后进行解锁

加锁

核心方法

解锁

我们使用redis的分布式锁来实现

首先引入maven依赖

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

编写加锁和解锁的方法:

@Component
@Slf4j
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key
     * @param value 当前事件+超时事件
     * @return
     */
    public boolean lock(String key,String value){
        //加锁成功
        if (redisTemplate.opsForValue().setIfAbsent(key,value)){
            return true;
        }
        //假如currentValue=A先占用了锁  其他两个线程的value都是B,保证其中一个线程拿到锁
        String currentValue = redisTemplate.opsForValue().get(key);
        //锁过期  防止出现死锁
        if (!StringUtils.isEmpty(currentValue) &&
                Long.parseLong(currentValue) < System.currentTimeMillis()){
            //获取上一步锁的时间
            String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
            if (!StringUtils.isEmpty(oldValue) &&
                        oldValue.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){
            log.error("【redis分布式锁】 解锁异常,{}",e);
        }
    }

}

我们修改原本的核心代码,对其进行加锁和解锁

@Service
public class SecKillServiceImpl implements SecKillService {
    /** 超时时间 */
    private static final int TIMOUT = 10000;

    @Autowired
    private RedisLock redisLock;

    /**
     * 中秋活动 秒杀月饼 限量100000
     */
    static Map<String, Integer> products;
    static Map<String, Integer> stock;
    static Map<String, String> orders;

    static {
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        products.put("1001", 100000);
        stock.put("1001", 100000);
    }

    private String queryMap(String productId) {
        return "中秋活动,月饼特价,限量份"
                + products.get(productId)
                + " 还剩:" + stock.get(productId) + " 份"
                + " 该商品成功下单用户数目:"
                + orders.size() + " 人";
    }


    @Override
    public String querySecKillProductInfo(String productId) {
        return queryMap(productId);
    }

    @Override
    public void orderProductMockDiffUser(String productId) {
        long time = System.currentTimeMillis() + TIMOUT;
        //加锁
        if (!redisLock.lock(productId,String.valueOf(time))){
            throw new SellException(110,"没抢到,换个姿势再来一遍");
        }
        int stockNum = stock.get(productId);
        if (stockNum == 0) {
            //库存不足
            throw new SellException(100, "活动已经结束,请留意下次活动");
        } else {

            orders.put(KeyUtils.getUniqueKey(), productId);
            stockNum = stockNum - 1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            stock.put(productId, stockNum);
        }
        //解锁
        redisLock.unlock(productId,String.valueOf(time));
    }

}

接下来我们测试一下:同样1000人同时访问

可以看到只有33人访问了进来,这是由于很多人访问时并没有拿到锁,所以直接跳过了。这样就处理了多线程并发问题的同时也保证了服务器的性能的稳定。

 

 

 

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值