网站首页 编程语言 正文
一般电商商品秒杀活动会面临大量用户同时下单的情况,不仅要面临高并发的问题,还要保证下单数量不超过商品数量和用户对同一商品不能重复下单(保证商品不被同一个用户抢购完,也就是防黄牛)。
面对这些问题,可以采用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
相关推荐
- 2022-09-06 Go 语言选择器实例教程_Golang
- 2024-04-05 @Version乐观锁配置mybatis-plus使用(version)
- 2021-11-27 Linux开机自启动服务两种方式介绍_Linux
- 2022-08-25 Asp.net core中依赖注入的实现_实用技巧
- 2022-11-02 解决SecureCRT通过SSH连接Ubuntu时vi命令有多余的m的问题_相关技巧
- 2023-03-15 Docker网络配置及部署SpringCloud项目详解_docker
- 2023-03-25 React高阶组件使用详细介绍_React
- 2022-06-17 docker修改容器配置文件的3种方法总结_docker
- 最近更新
-
- window11 系统安装 yarn
- 超详细win安装深度学习环境2025年最新版(
- Linux 中运行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存储小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基础操作-- 运算符,流程控制 Flo
- 1. Int 和Integer 的区别,Jav
- spring @retryable不生效的一种
- Spring Security之认证信息的处理
- Spring Security之认证过滤器
- Spring Security概述快速入门
- Spring Security之配置体系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置权
- redisson分布式锁中waittime的设
- maven:解决release错误:Artif
- restTemplate使用总结
- Spring Security之安全异常处理
- MybatisPlus优雅实现加密?
- Spring ioc容器与Bean的生命周期。
- 【探索SpringCloud】服务发现-Nac
- Spring Security之基于HttpR
- Redis 底层数据结构-简单动态字符串(SD
- arthas操作spring被代理目标对象命令
- Spring中的单例模式应用详解
- 聊聊消息队列,发送消息的4种方式
- bootspring第三方资源配置管理
- GIT同步修改后的远程分支