三、Java秒杀问题_13(笔记)

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倒计时)

image-20230813140641917

单个人到0点击抢购

image-20230813140710131

成功

image-20230813140951227

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

image-20230813141116626

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

image-20230813141255266

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

image-20230813141441086

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

image-20230813141655994

总结

多个人秒杀同一个商品: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缓存

image-20230813230543084

用Apipost测试

image-20230813230708424

再次查看redis

image-20230813230907667

image-20230813231020776

多个人测试:

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

image-20230813231800623

image-20230813231927984

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

image-20230813232009150

image-20230813232049002

第五个人抢购详情

image-20230813232203494

image-20230813232241036

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

总结

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

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

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

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

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

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

概括

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值