Java-redis实现限时在线秒杀功能

1.使用redisson pom文件添加redisson

复制代码
 <!--redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.23.4</version>
        </dependency>

2.mysql数据库表设计

复制代码
CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `product_id` bigint(20) NOT NULL COMMENT '商品ID',
  `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态(0:未支付,1:已支付,2:已取消)',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1929810703233400835 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';




CREATE TABLE `product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `name` varchar(100) NOT NULL COMMENT '商品名称',
  `price` decimal(10,2) NOT NULL COMMENT '商品价格',
  `stock` int(11) NOT NULL COMMENT '库存数量',
  `start_time` datetime DEFAULT NULL COMMENT '秒杀开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '秒杀结束时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1929810179796844547 DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';


CREATE TABLE `seckill_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `product_id` bigint(20) NOT NULL COMMENT '商品ID',
  `order_no` varchar(50) DEFAULT NULL COMMENT '订单编号',
  `seckill_time` datetime NOT NULL COMMENT '秒杀时间',
  `status` tinyint(4) NOT NULL COMMENT '状态(0:秒杀成功未支付,1:已支付,2:已取消)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_product` (`user_id`,`product_id`) COMMENT '用户商品唯一索引',
  KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1929810703233400836 DEFAULT CHARSET=utf8mb4 COMMENT='秒杀记录表';

3.实现逻辑

1.新增秒杀商品,添加库存数量,设置秒杀开始时间和结束时间

2.确认开始秒杀,将库存数量入redis中,避免进行超卖

3.进行秒杀

3.1.根据商品id查询商品是否存在

3.2.判断当前时间是否在秒杀开始时间

3.3. 获取Redisson锁

3.4.判断当前人是否已经秒杀过,避免重复秒杀

3.5.判断库存是否充足,避免超卖,库存不足,返回商品已售罄

3.6.判断库存充足,扣减库存并记录用户

3.7.秒杀成功创建订单,返回订单号

3.8.关闭锁,避免死锁

4.代码实现

1.秒杀控制器

复制代码
@RestController
@RequestMapping("/seckill")
public class SeckillController {
    @Autowired
    private SeckillService seckillService;

    /**
     *  确认开始秒杀,初始化商品库存到Redis
     * @param product
     * @return
     */
    @PostMapping("/initStock")
    public AjaxResult initStock(@RequestBody Product product) {
        return seckillService.initStock(product);
    }

    /**
     * 开始秒杀
     * @return
     */
    @PostMapping("/start")
    public AjaxResult startSeckill(Long userId, Long productId) {
        return seckillService.startSeckill(userId,productId);
    }

}

2.秒杀实现类

复制代码
package com.thk.service.impl;

import com.thk.domain.Order;
import com.thk.domain.Product;
import com.thk.domain.SeckillRecord;
import com.thk.mapper.OrderMapper;
import com.thk.mapper.ProductMapper;
import com.thk.mapper.SeckillRecordMapper;
import com.thk.service.SeckillService;
import com.thk.utils.AjaxResult;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private SeckillRecordMapper seckillRecordMapper;
    @Autowired
    private RedissonClient redissonClient;

    // 商品库存key前缀
    private static final String STOCK_PREFIX = "seckill:stock:";
    // 秒杀用户集合key前缀(用于防止重复秒杀)
    private static final String USER_SET_PREFIX = "seckill:users:";
    // 分布式锁key前缀
    private static final String LOCK_PREFIX = "seckill:lock:";

    /**
     * 确认开始秒杀,初始化商品库存到Redis
     *
     * @param product
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult initStock(Product product) {
        Product product1 = productMapper.selectById(product.getId());
        if (product1 != null) {
            String stockKey = STOCK_PREFIX + product1.getId();
            RAtomicLong atomicLong = redissonClient.getAtomicLong( stockKey );
            atomicLong.set(product1.getStock());

            // 设置过期时间(秒)
            long expireSeconds = (product1.getEndTime().getTime() - System.currentTimeMillis()) / 1000;
            boolean expireResult = atomicLong.expire(expireSeconds, TimeUnit.SECONDS);

            return AjaxResult.success(expireResult);
        }
        return AjaxResult.error("商品不存在");
    }



    /**
     * 开始秒杀
     *
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)

    public AjaxResult startSeckill(Long userId, Long productId) {
        // 1. 验证秒杀时间
        Product product = productMapper.selectById(productId);
        if (product == null) return AjaxResult.error("商品不存在");

        long now = System.currentTimeMillis();
        if (now < product.getStartTime().getTime()) {
            return AjaxResult.error("秒杀未开始");
        }
        if (now > product.getEndTime().getTime()) {
            return AjaxResult.error("秒杀已结束");
        }

        // 2. 获取Redisson分布式锁
        String lockKey = LOCK_PREFIX + productId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) return AjaxResult.error("系统繁忙,请稍后再试");

            // 3. 使用Redisson的RSet检查重复秒杀
            String userKey = USER_SET_PREFIX + productId;
            RSet<String> userSet = redissonClient.getSet(userKey);
            if (userSet.contains(userId.toString())) {
                return AjaxResult.error("不能重复秒杀");
            }

            // 4. 使用Redisson的RAtomicLong检查库存
            String stockKey = STOCK_PREFIX + productId;
            RAtomicLong atomicStock = redissonClient.getAtomicLong(stockKey);
            if (atomicStock.get() <= 0) {
                return AjaxResult.error("商品已售罄");
            }

            // 5. 扣减库存并记录用户
            atomicStock.decrementAndGet();
            userSet.add(userId.toString());

            // 6. 创建订单
            String orderNo = generateOrderNo();
            createOrder(userId, productId, orderNo);
            return AjaxResult.success(orderNo);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return AjaxResult.error("系统异常");
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                //关闭锁
                lock.unlock();
            }
        }
    }




    private void createOrder(Long userId, Long productId, String orderNo) {
        Order order = new Order();
        order.setUserId( userId );
        order.setProductId( productId );
        order.setOrderNo( orderNo );
        order.setStatus( 0 );
        order.setCreateTime( new Date() );
        orderMapper.insert( order );

        SeckillRecord record = new SeckillRecord();
        record.setUserId( userId );
        record.setProductId( productId );
        record.setOrderNo( orderNo );
        record.setSeckillTime( new Date() );
        record.setStatus( 0 );
        seckillRecordMapper.insert( record );
    }

    private String generateOrderNo() {
        return "O" + System.currentTimeMillis();
    }
}

5.测试

1.添加秒杀商品,设置库存,开始时间,结束时间

2.确认开始,将库存存入redis中

redis查看库存是否存在

3.开始秒杀,输入用户id和商品编号进行秒杀

秒杀成功,创建订单,新增商品秒杀记录,返回订单号,redis库存减少,redis新增用户秒杀记录

订单记录

秒杀记录

redis库存

redis新增用户秒杀记录

4.重复秒杀

5.避免超卖

6.秒杀结束,超时秒杀结束

相关推荐
BillKu1 小时前
Java + Spring Boot + Mybatis 实现批量插入
java·spring boot·mybatis
YuTaoShao1 小时前
Java八股文——集合「Map篇」
java
❀͜͡傀儡师1 小时前
如何使用k8s安装redis呢
redis·容器·kubernetes
有梦想的攻城狮3 小时前
maven中的maven-antrun-plugin插件详解
java·maven·插件·antrun
硅的褶皱7 小时前
对比分析LinkedBlockingQueue和SynchronousQueue
java·并发编程
MoFe17 小时前
【.net core】天地图坐标转换为高德地图坐标(WGS84 坐标转 GCJ02 坐标)
java·前端·.netcore
潘yi.7 小时前
NoSQL之Redis配置与优化
数据库·redis·nosql
伤不起bb7 小时前
NoSQL 之 Redis 配置与优化
linux·运维·数据库·redis·nosql
季鸢7 小时前
Java设计模式之观察者模式详解
java·观察者模式·设计模式
Fanxt_Ja8 小时前
【JVM】三色标记法原理
java·开发语言·jvm·算法