日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

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

欄目分類
最近更新