文章目录

  • java 中秒杀逻辑
  • 前言
  • 什么是秒杀?
  • 特点
  • 考虑要点
  • 预减库存
  • 单个商品的秒杀
  • 导入依赖
  • bean包
  • application.yml
  • util包
  • SpringBootSecKillApplication
  • service包
  • controller包
  • 前端部分
  • 运行结果
  • 总结
  • 多个商品的秒杀
  • bean包
  • service包
  • 启动类
  • 前端页面
  • 运行结果
  • 总结


java 中秒杀逻辑

前言

什么是秒杀?

是一种高并发的技术,许多电商网站都是采用这样的技术应对突发流量的问题。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大量用户前来抢购,并且会在约定的时间点同时在秒杀页面进行抢购。

特点

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 秒杀业务流程比较简单,一般就是下订单减库存。

考虑要点

1、查询数据库的信息,查看该商品是否存在
2、验证购买上线
3、进行秒杀时间的比较,确保活动有效时间内
4、查询用户是否下过订单,防止重复下单
5、查看库是否存在
6、生成订单
7、订单生成成功后进行减库存

预减库存

服务启动时,去将秒杀商品缓存到redis
    库存判断通过redis来做
    加锁通过秒杀订单数量 是不是和秒杀库存一致

单个商品的秒杀

运用分布式锁解决(为了避免多线程导致的数据不一致性,采用锁机制保护共享资源)

导入依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>
    </dependencies>
bean包

Goods

@Data
public class Goods implements Serializable {
    // 商品编号
    private Integer id;
    // 商品名称
    private String name;
    // 库存数量
    private Integer stock;
    // 购买时间
    private String buyTime;
    // 购买者(模拟了购买者的IP信息)
    private String ip;
    // 商品价格
    private Double price;
    // 商品秒杀开始时间
    private Date createTime;

}
application.yml
spring:
  redis:
    host: 192.168.65.3
util包

JsonResult

@Data
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<T> implements Serializable {
    private Boolean success;
    private String error;
    private Integer code;
    private T data;
}

ResultTool

public class ResultTool {

    public static JsonResult success() {
        return new JsonResult(true, null, 200, null);
    }

    public static JsonResult success(Object data) {
        return new JsonResult(true, null, 200, data);
    }

    public static JsonResult fail(String msg) {
        return new JsonResult(false, msg, 500, null);
    }
}
SpringBootSecKillApplication
@SpringBootApplication
public class SpringBootSecKillApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringBootSecKillApplication.class, args);
        SecKillOneServiceImpl service = run.getBean(SecKillOneServiceImpl.class);
        service.init();
    }
}
service包

SecKillService

public interface SecKillService {

    JsonResult killOneGoods(String ip);
}

impl——>SecKillOneServiceImpl

@Slf4j
@Service
public class SecKillOneServiceImpl implements SecKillService {


    @Resource
    private StringRedisTemplate stringRedisTemplate;
    private Date date;

    public void init() {
        //模拟一个商品
        Goods goods = new Goods();
        goods.setId(1);
        goods.setName("可口可乐");
        goods.setPrice(6.6);
        goods.setStock(1);
        //模拟开始时间是10s后
        new Date(new Date().getTime() + 10000);
        log.info("秒杀开始于:{}", date);
        goods.setCreateTime(date);
        stringRedisTemplate.opsForValue().set("GOODS", JSONArray.toJSONString(goods));

    }


    @Override
    public JsonResult killOneGoods(String ip) {
        ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
        //商品是否已经被购买了
        String str = valueOperations.get("GOODS");
        log.info("商品是:{}", str);
        if (str == null) {
            return ResultTool.fail("商品已被抢购,感谢惠顾!");
        }
        //判断是否在秒杀开始时间之后进行
        Date date = new Date();
        log.info("开始时间是:{}", date);
        log.info("秒杀开始时间是:{}", this.date);
        if (this.date.after(date)) {
            return ResultTool.fail("秒杀还没有开始,请耐心等待!");
        }
        //redis的分布锁
        //首先上锁,如果能上锁,你就抢到了,如果不能上锁,说明该商品已被其他人抢购,快速失败
        boolean flag = valueOperations.setIfAbsent("A", "1");
        log.info("是否上锁:{}", flag);
        if (flag) {
            //上锁成功,订单就是你的了
            Goods goods = JSONArray.parseObject(str, Goods.class);
            goods.setIp(ip);
            goods.setBuyTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            //删除该商品
            valueOperations.getAndDelete("GOODS");
            //释放锁
            valueOperations.getAndDelete("A");
            log.info("抢购成功,商品是:{}", goods);
            return ResultTool.success(goods);
        }
        log.info("抢购失败");
        //上锁失败,快锁失败
        return ResultTool.fail("商品已被抢购,感谢惠顾!");
    }
}
controller包

SecKillController

@RestController
@RequestMapping("/seckill")
public class SecKillController {

    @Resource
    private SecKillService secKillOneServiceImpl;

    @GetMapping("/one")
    public JsonResult one(HttpServletRequest request) {
        return secKillOneServiceImpl.killOneGoods(request.getRemoteAddr());
    }

}
前端部分

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <h2 style="color: red">
        <span>{{num}}</span>
    </h2>
    <h4>{{result}}</h4>
    <br/>
    <button @click="beginSecKill">开始秒杀</button>
</div>
</body>
</html>
<script src="js/vue.min.js"></script>
<script src="js/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data() {
            return {
                num: 10,
                result: ''
            }
        },
        methods: {
            beginSecKill() {
                if (this.num > 0) {
                    alert('秒杀还没有开始,请稍后!');
                    return;
                }
                let _this = this
                axios.get('seckill/one')
                    .then((response) => {
                        if (response.data.success) {
                            _this.result = '秒杀成功' + response.data.data
                        } else {
                            _this.result = response.data.error
                        }
                    })
            },
            daojishi() {
                this.num--;
                let _this = this
                if (this.num > 0) {
                    setTimeout(function () {
                        _this.daojishi()
                    }, 1000);
                }
            }
        },
        created() {
            this.daojishi()
        }
    })
</script>
运行结果

启动项目(10倒计时)

java 促销系统_java

单个人到0点击抢购

java 促销系统_java_02

成功

java 促销系统_java 促销系统_03

查看redis中,发现抢完就被删除,

java 促销系统_java_04

多个人到0点击抢购,通过IP地址

java 促销系统_笔记_05

通过控制台,发现此用户抢到了

java 促销系统_分布式锁_06

注意:通过上述测试,发现倒计时有问题,没有和后端联动上

java 促销系统_java 促销系统_07

总结

多个人秒杀同一个商品:redis分布式锁

当多个用户同时竞争同一件商品,如果不进行并发控制,可能会导致超卖商品库存出现负数等情况。

使用 Redis 分布式锁可以确保只有一个用户能够获得锁并抢购商品,其他用户在锁被释放之前无法进行抢购操作。

首先进行一些判断,秒杀是否已开始、是否已购买、库存是否满足等。如果不满足条件,返回相应的失败信息。

若满足秒杀条件,通过redis分布式锁进行秒杀机制。

假设通过 valueOperations.setIfAbsent(“”, “”) 方法在 Redis 中设置键 进行上锁操作,如果成功返回 true,表示上锁成功,则表示用户抢购成功

删除 Redis 中键 对应的商品信息,并释放锁。最后返回抢购成功的结果,包括商品信息。

如果上锁失败,则表示该商品已被其他用户抢购,抢购失败

概括

单个商品秒杀(redis分布式锁)
先进行判断,是否在秒杀开始时间之后进行,查看ip是否已购买,查看库存是否满足。若不满足条
件,返回相应的失败信息。
如果满足秒杀条件,通过redis分布式锁进行秒杀机制。
1、若成功上锁,则表示抢购成功;
2、若无法上锁,说明该商品已经被其他用户抢购,快速失败并返回给用户,确保此时只有一个用户可以抢到商品,其他用户在解锁之前无法抢购商品,在用户抢购成功后删除数据并释放锁。
3、当释放完锁后,有的用户有可能会有第二次抢购,因此需要判断该商品是否已经被购买,已经购买的商品通过标识符或秒杀符删除,返回结果为flase,则上锁失败,将失败信息返回给用户。

多个商品的秒杀

问题:现有五本书,进行秒杀,如何保证被五个用户抢走,不会被第六个人抢走,可以少卖,但不能出现超卖呢?

思路:现有五本书(1、2、3、4、5),通过List集合存放到redis中去,用户每抢走一个,就删去一个,在删的同时得记录是谁买了哪本书,给每一本书起一个唯一的标识符或秒杀符,又利用Map集合记录哪个ip买了那本书(对应的有标识符),当用户秒杀成功后,会在Map集合中有记录,如果秒杀成功的用户再次想秒杀就不行 。还有一个问题,当库存量小于0时,就会出现超卖的情况,因此需要对库存量进行监控,当监控时有第三方修改了库存量,所有的操作都会失败。这种监控的好处有两个:1、一次保证只有一个用户操作;2、失败了不会阻塞线程。

通过redist的事务解决

bean包

Goods

// 每个商品ID
    private String secKillId;
service包

SecKillService

JsonResult killManyGoods(String ip);

SecKillOneServiceImpl

@Override
    public JsonResult killManyGoods(String ip) {
        return null;
    }

impl–>SecKillManyServiceImpl

@Slf4j
@Service
public class SecKillManyServiceImpl implements SecKillService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    private Date date;

    public void init(){
        //初始化数据
        for (int i = 1; i <6 ; i++) {
            Goods goods = new Goods();
            goods.setId(i);
            goods.setPrice(6.5);
            goods.setName("可口可乐");
            //模拟开始时间是10s后
            date=new Date(new Date().getTime()+1000);
            log.info("秒杀开始于:{}",date);
            goods.setCreateTime(date);
            goods.setSecKillId(UUID.randomUUID().toString());
            stringRedisTemplate.opsForList().rightPush("GOODS_LIST", JSONArray.toJSONString(goods));
        }
        //设置库存
        stringRedisTemplate.opsForValue().set("STOCK","5");
        //设置开始时间
        stringRedisTemplate.opsForValue().set("CREATE_TIME",JSONArray.toJSONString(date));
    }

    @Override
    public JsonResult killOneGoods(String ip) {
        return null;
    }

    @Override
    public JsonResult killManyGoods(String ip) {
        log.info("------------------------------------------------------");
        //设置专属用户抢占
         /*if (!ip.equals("127.0.0.1")) {
            return ResultTool.fail("你的IP不能参与抢购!");
        }*/
        //判断是否在秒杀开始之后进行
        Date date = new Date();
        log.info("当前时间是:{}", date);
        log.info("秒杀开始时间是:{}", this.date);
        log.info("ip{}的用户开始抢购", ip);
        if (this.date.after(date)) {
            log.info("{}用户,秒杀还没有开始", ip);
            return ResultTool.fail("秒杀还没有开始,请耐心等候!");
        }
        // 查看ip是否已购买
        boolean flag = stringRedisTemplate.opsForHash().hasKey("ORDER", ip);
        if (flag) {
            log.info("{}用户,你已经购买", ip);
            return ResultTool.fail("你已经购买这个商品了,请给其他用户一些机会!");
        }
        // 查看库存是否满足
        String stock = stringRedisTemplate.opsForValue().get("STOCK");
        if (stock == null || Integer.parseInt(stock) <= 0) {
            log.info("{}用户,抢购结束", ip);
            return ResultTool.fail("抢购失败,请下次关注!");
        }
        // 通过事务实现秒杀
        // 监控key:STOCK
        stringRedisTemplate.watch("STOCK");
        SessionCallback<List<String>> sessionCallback = new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // 开启事务
                stringRedisTemplate.multi();
                // 减少库存
                stringRedisTemplate.opsForValue().decrement("STOCK");
                // 出货
                stringRedisTemplate.opsForList().rightPop("GOODS_LIST");
                // 添加购买记录
                stringRedisTemplate.opsForHash().put("ORDER", ip, "");
                // 执行事务
                return stringRedisTemplate.exec();
            }
        };
        List<String> list = stringRedisTemplate.execute(sessionCallback);
        log.info("抢购后监控状态:{},数量是:{}", list.get(1), list.get(0));
        if (list.get(1) == null || list.get(1).equals("")) {
            return ResultTool.fail("抢购失败,请下次关注!");
        }
        // 解除监控key:STOCK
        stringRedisTemplate.unwatch();
        return ResultTool.success(list.get(1));
    }
}
启动类

SpringBootSecKillApplication

@SpringBootApplication
public class SpringBootSecKillApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringBootSecKillApplication.class, args);

        //单个
//        SecKillOneServiceImpl service = run.getBean(SecKillOneServiceImpl.class);
//        service.init();
        //多个
        SecKillManyServiceImpl service = run.getBean(SecKillManyServiceImpl.class);
        service.init();
    }
}
前端页面

index1.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app">
  <h4>{{result}}</h4>
  <br/>
  <button @click="beginSecKill">开始秒杀</button>
</div>
</body>
</html>
<script src="js/vue.min.js"></script>
<script src="js/axios.min.js"></script>
<script>
  new Vue({
    el: '#app',
    data() {
      return {
        num: 10,
        result: ''
      }
    },
    methods: {
      beginSecKill() {
        let _this = this
        axios.get('seckill/many')
                .then((response) => {
                  if (response.data.success) {
                    // _this.result = '秒杀成功' + response.data.data
                    location.href = 'https://www.baidu.com/'
                  } else {
                    _this.result = response.data.error
                  }
                })
      }
    }
  })
</script>
运行结果

单个人测试:

启动项目,查看redis缓存

java 促销系统_分布式锁_08

用Apipost测试

java 促销系统_java 促销系统_09

再次查看redis

java 促销系统_java 促销系统_10

java 促销系统_redis_11

多个人测试:

第一个和第二个人抢购详情

java 促销系统_java_12

java 促销系统_分布式锁_13

第三个和第四个人抢购详情

java 促销系统_java 促销系统_14

java 促销系统_分布式锁_15

第五个人抢购详情

java 促销系统_java 促销系统_16

java 促销系统_笔记_17

注:五个人抢购完成,库存清零,秒杀结束

总结

多个人秒杀多个商品:redis事务

当多个用户秒杀多个商品时,通过 Redis 事务可以确保减少库存、出货和添加购买记录这些操作作为一个整体进行执行,避免了其中的任何一个操作在执行过程中被其他请求干扰导致数据不一致的问题。

首先进行一些判断秒杀是否已开始、是否已购买、库存是否满足、是否有人重复购买等。如果不满足条件,返回相应的失败信息。

若满足秒杀条件,则通过 Redis 的事务机制进行秒杀操作。

首先对键 ”库存“ 进行监控,然后开启事务,在事务中执行减少库存、出货、添加购买记录。通过exec()方法获取事务执行的结果。

如果事务执行成功并返回了购买的商品信息,则表示秒杀成功。否则,返回秒杀失败的信息。事务执行后解除对键 “库存” 的监控。

概括

多个商品秒杀(redis事务)
利用Redis事务来防止并发冲突导致的数据问题。
在高并发情况下,可能出现的超卖情况,多线程导致数据不一致等情况,可以通过使用redis事务,比如库存量小于0时,就会出现超卖的情况,因此需要对库存量进行监控,当监控时有第三方修改了库存量,所有的操作都会失败。利用watch监控库存量,在事务中执行,减少库存,出货,添加购买记录等。
1、如果事务执行成功并返回了购买的商品信息,则表示秒杀成功;
2、如果在事务执行过程中,库存量发生更改,则事务直接结束,并返回秒杀失败的信息,事务执行后解除对库存量的监控。