学无先后,达者为师

网站首页 编程语言 正文

Redisson之分布式锁解决商品秒杀简单示例

作者:wl_Honest 更新时间: 2022-10-14 编程语言

一般电商商品秒杀活动会面临大量用户同时下单的情况,不仅要面临高并发的问题,还要保证下单数量不超过商品数量和用户对同一商品不能重复下单(保证商品不被同一个用户抢购完,也就是防黄牛)。

面对这些问题,可以采用Redis分布锁来解决,通过Redis中setnx命令来保证同一时间只有一个线程能够正常下单,待订单创建成功后解锁,其余线程再来抢锁。

首先模拟一下未采用Redis加锁的代码实现,创建了3张表:用户表、商品表和订单表

 

 

maven依赖:

<dependencies>
        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--集成mysql数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </dependency>
        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!--springboot中的redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- lettuce pool 缓存连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <!--处理JSON格式-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.20</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions><!-- 去掉springboot默认配置 -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- spring boot 2.3版本后,如果需要使用校验,需手动导入validation包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency> <!-- 引入log4j2依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>
    </dependencies>

Controller层:

package com.wl.demo.controller;

import cn.hutool.core.util.StrUtil;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author wl
 * @date 2022/4/6
 */
@RestController
@RequestMapping("/order")
public class OrderController {

    private final OrderService orderService;

    @Autowired
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping("/add")
    public HttpResult addOrder(@RequestBody String body) {
        if (StrUtil.isBlank(body)) {
            return HttpResult.fail("请求体不能为空");
        }
        return orderService.addOrder(body);
    }
}

Service实现类:

package com.wl.demo.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.entity.Order;
import com.wl.demo.mapper.OrderMapper;
import com.wl.demo.service.OrderService;
import com.wl.demo.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author wl
 * @date 2022/4/6
 */
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    private final ProductService productService;

    @Autowired
    public OrderServiceImpl(ProductService productService) {
        this.productService = productService;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public HttpResult addOrder(String body) {
        Order order = JSONObject.toJavaObject(JSONObject.parseObject(body), Order.class);
        // 查询订单
        Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
        if (count > 0) {
            log.error("不允许重复下单");
            return HttpResult.fail("不允许重复下单");
        }
        // 扣减库存
        boolean success = productService.update()
                    .setSql("stock = stock - 1")
                    .eq("product_id", order.getProductId()).gt("stock", 0) // where id = ? and stock > 0
                    .update();
        if (!success) {
            log.error("库存不足");
            return HttpResult.fail("库存不足");
        }
        // 创建订单
        save(order);
        return HttpResult.success();
    }
}

启动项目,打开jmeter模拟200个用户(同一个)同时抢

 查看订单表:

 商品表:

 虽然避免了下单数量多于商品库存数量,但还是出现重复下单的情况。

接下来就轮到Redisson出场

首先定义Redisson配置类:

package com.wl.demo.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author wl
 * @date 2022/4/6
 */
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redisClient() {
        Config config = new Config();
        //这里ip和port改成自己的redis地址和端口
        config.useSingleServer().setAddress("redis://ip:port");
        return Redisson.create(config);
    }
}

然后修改刚刚的Service实现类方法:

package com.wl.demo.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.entity.Order;
import com.wl.demo.mapper.OrderMapper;
import com.wl.demo.service.OrderService;
import com.wl.demo.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author wl
 * @date 2022/4/6
 */
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    private final static String LOCK_ORDER_KEY_PREFIX = "lock:order:";

    private final RedissonClient redissonClient;
    private final ProductService productService;

    @Autowired
    public OrderServiceImpl(RedissonClient redissonClient, ProductService productService) {
        this.redissonClient = redissonClient;
        this.productService = productService;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public HttpResult addOrder(String body) {
        Order order = JSONObject.toJavaObject(JSONObject.parseObject(body), Order.class);
        // 创建锁对象
        String lockKey = LOCK_ORDER_KEY_PREFIX + order.getUserId();
        RLock redisLock = redissonClient.getLock(lockKey);
        boolean isLocked = redisLock.tryLock();
        if (!isLocked) {
            log.error("不允许重复下单");
            return HttpResult.fail("不允许重复下单");
        }

        try {
            // 查询订单
            Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
            if (count > 0) {
                log.error("不允许重复下单");
                return HttpResult.fail("不允许重复下单");
            }
            //扣减库存
            boolean success = productService.update()
                    .setSql("stock = stock - 1")
                    .eq("product_id", order.getProductId()).gt("stock", 0) // where id = ? and stock > 0
                    .update();
            if (!success) {
                log.error("库存不足");
                return HttpResult.fail("库存不足");
            }
            // 插入数据库
            save(order);
        } finally {
            redisLock.unlock();
        }
        return HttpResult.success();
    }
}

启动之前,删除订单表数据,恢复商品表库存为100:

重新模拟200个用户(同一用户)同时下单:

 

 订单表:

商品表:

 

 可以看到,利用Redisson加锁后,有效避免了用户重复下单的问题,需要注意的是,加锁的方法tryLock()这里用的是Redisson默认的参数,其实还可以自己指定参数。比如lock.tryLock(10,10, TimeUnit.SECONDS)就是尝试加锁,最多等待10秒,上锁以后10秒自动解锁,这个可以根据自己的实际情况来做调整。

原以为代码这样写就万无一失了,于是又测试了一下500个请求秒杀,结果发现竟然有漏网之鱼

 订单表

 商品表

虽然200个请求没出问题,但稍微一加大并发量就暴露了问题,针对这种情况,如果是单机部署可以考虑使用 ConcurrentHashMap来存放秒杀成功的用户,每次加锁成功后判断集合里是否有该id的用户,如果有,则说明该用户已经下单成功。如果是集群部署的话,那就可以考虑采用redis来缓存秒杀成功的用户,这样每台机器上的服务都能访问redis来判断该用户是否已经下单成功。

代码只需稍微改动一下,增加一个RedisUtils类

package com.wl.demo.util;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author wl
 * @date 2022/4/6
 */
@Component
@Slf4j
public class RedisUtils {

    private final RedisTemplate<String, String> stringRedisTemplate;

    @Autowired
    public RedisUtils(RedisTemplate<String, String> stringRedisTemplate, RedissonClient redissonClient) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public String getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    public void setValue(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    public void setValue(String key, String value, Long expireTime, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, value, expireTime, timeUnit);
    }

    public Boolean isExistKey(String key) {
        return stringRedisTemplate.hasKey(key);
    }
}

Service实现类稍作修改:

package com.wl.demo.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.entity.Order;
import com.wl.demo.mapper.OrderMapper;
import com.wl.demo.service.OrderService;
import com.wl.demo.service.ProductService;
import com.wl.demo.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

/**
 * @author wl
 * @date 2022/4/6
 */
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    private final static String LOCK_ORDER_KEY_PREFIX = "lock:order:";
    private final static String CACHE_ORDER_KEY_PREFIX = "cache:order:";

    private final RedisUtils redisUtils;
    private final RedissonClient redissonClient;
    private final ProductService productService;

    @Autowired
    public OrderServiceImpl(RedisUtils redisUtils, RedissonClient redissonClient, ProductService productService) {
        this.redisUtils = redisUtils;
        this.redissonClient = redissonClient;
        this.productService = productService;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public HttpResult addOrder(String body) {
        Order order = JSONObject.toJavaObject(JSONObject.parseObject(body), Order.class);
        // 创建锁对象
        String lockKey = LOCK_ORDER_KEY_PREFIX + order.getUserId();
        RLock redisLock = redissonClient.getLock(lockKey);
        boolean isLocked = redisLock.tryLock();
        if (!isLocked) {
            log.error("不允许重复下单");
            return HttpResult.fail("不允许重复下单");
        }
        String key = CACHE_ORDER_KEY_PREFIX + order.getUserId();
        if (redisUtils.isExistKey(key)) {
            log.error("不允许重复下单");
            return HttpResult.fail("不允许重复下单");
        }

        try {
            // 查询订单
            Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
            if (count > 0) {
                log.error("不允许重复下单");
                return HttpResult.fail("不允许重复下单");
            }
            //扣减库存
            boolean success = productService.update()
                    .setSql("stock = stock - 1")
                    .eq("product_id", order.getProductId()).gt("stock", 0) // where id = ? and stock > 0
                    .update();
            if (!success) {
                log.error("库存不足");
                return HttpResult.fail("库存不足");
            }
            //存入秒杀成功的用户
            redisUtils.setValue(key, JSONObject.toJSONString(order), 60L, TimeUnit.SECONDS);
            // 插入数据库
            save(order);
        } finally {
            redisLock.unlock();
        }
        return HttpResult.success();
    }
}

最后为了测试的严谨,在本机复制这个项目,端口改为8081,同时开启2个服务

 然后用nginx代理这2个端口

 依然用jmeter测试500个并发请求

 测试完的用户表

 商品表

 redis缓存的下单成功的用户

 到此,简单的商品秒杀就实现了。

 

原文链接:https://blog.csdn.net/wl_Honest/article/details/124006944

栏目分类
最近更新