实现秒杀功能

本文详细介绍了如何构建一个高并发秒杀系统,包括基础环境搭建、使用悲观锁和乐观锁防止超卖、令牌桶限流、Redis设置秒杀时间、接口隐藏以及单用户访问频率限制等策略。通过这些技术手段,确保了秒杀过程的稳定性和数据一致性。
摘要由CSDN通过智能技术生成

搭建基础环境

entity

Order.java

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Order {
    private Integer id;
    private Integer sid;
    private String name;
    private Date createDate;
}

Stock.java

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Stock {
    private Integer id;
    private String name;
    private Integer count;
    private Integer sale;
    private Integer version;
}

dao

OrderDAO.java

@Repository
public interface OrderDAO {
    //创建订单方法
    void createOrder(Order order);
}

StockDAO.java

@Repository
public interface StockDAO {
    //根据商品Id查询库存信息的方法
    Stock checkStock(Integer id);

    //根据ID扣除库存
    void updateSale(Stock stock);
}

service

OrderService.java

public interface OrderService {
    //用来处理秒杀的下单方法,并返回订单id
    int kill(Integer id);
}

OrderServiceImpl.java

@Service
@Transactional
public class OrderServiceImpl implements OrderService {

    @Autowired
    private StockDAO stockDAO;

    @Autowired
    private OrderDAO orderDAO;

    @Override
    public int kill(Integer id) {
        //根据商品id校验库存
        Stock stock = stockDAO.checkStock(id);
        if (stock.getSale().equals(stock.getCount())){
            throw new RuntimeException("库存不足!!!");
        }else{
            //扣除库存
            stock.setSale(stock.getSale()+1);
            stockDAO.updateSale(stock);
            //创建订单
            Order order = new Order();
            order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
            orderDAO.createOrder(order);
            return order.getId();
        }

    }
}

mapper

OrderDAOMapper.xml

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.axiang.dao.OrderDAO">
<!--创建订单-->
    <insert id="createOrder" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
        insert into stock_order values(#{id},#{name},#{sid},#{createDate})
    </insert>

</mapper>

StockDAOMapper.xml

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.axiang.dao.StockDAO">
<!--    根据id查询库存-->
    <select id="checkStock" parameterType="int" resultType="Stock">
        select id,name,count,sale,version from stock
        where id = #{id}
    </select>
    <update id="updateSale" parameterType="Stock">
        update stock set sale = #{sale} where id = #{id}
    </update>
</mapper>

搭建高并发测试环境

去https://jmeter.apache.org/download_jmeter.cgi下载jmeter

解压+配置环境变量

在这里插入图片描述

GUI运行:在bin下双击jmeter.bat运行(不推荐)

命令行运行

在bin下cmd;

输入命令:

jmeter -n -t C:\Users\Administrator\Desktop\miaosha.jmx -l C:\Users\Administrator\Desktop\miaosha.txt -e -o Desktop.html

悲观锁解决超卖

在方法处加上synchronized关键字

在这里插入图片描述

比库存超出一点

在这里插入图片描述

在这里插入图片描述

原因:

事务的线程范围比synchronized大:synchronized的代码块结束后,事务还没有结束,但是锁已经释放了且下一个线程来了,又把下一个线程和数据库提交了。所以会产生多提交的问题。

事务注解加方法上就没事了,一般也不会加在类上面

解决:

  • 把事务注解去掉(但是库存不足时需要事务控制程序不再创建订单)
  • 把同步代码块放在控制器调用处:(使线程(synchronized)的执行范围比事务大)

在这里插入图片描述

乐观锁解决超卖

优化代码OrderService.java

@Service
@Transactional
public class OrderServiceImpl implements OrderService {

    @Autowired
    private StockDAO stockDAO;

    @Autowired
    private OrderDAO orderDAO;

    @Override
    public int kill(Integer id) {
        //校验库存
        Stock stock = checkStock(id);
        //更新库存
        updateSale(stock);
        //创建订单
        return createOrder(stock);
        }
        //校验库存
        private Stock checkStock(Integer id){
            Stock stock = stockDAO.checkStock(id);
            if (stock.getSale().equals(stock.getCount())){
                throw new RuntimeException("库存不足!!!");
            }
            return stock;
        }
        //扣除库存
        private void updateSale(Stock stock){
            stock.setSale(stock.getSale()+1);
            stockDAO.updateSale(stock);
        }
        //创建订单
        private Integer createOrder(Stock stock){
            Order order = new Order();
            order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());
            orderDAO.createOrder(order);
            return order.getId();
        }

    }

使用乐观锁实际上是把防止超卖问题交给数据库解决,利用数据库中定义的 version字段以及数据库中的事务现在并发情况下商品的超卖问题

1.更新库存方法改造

//扣除库存
private void updateSale(Stock stock){
    //在sql层面完成销量的+1 和版本号的 +1 并且根据商品id和版本号同时查询更新的商品
    stockDAO.updateSale(stock);
}
<update id="updateSale" parameterType="Stock">
   update stock set
   sale = sale + 1,
   version = version + 1
   where
    id = #{id}
    and
    version = #{version}
</update>

2.再次优化库存方法

        //扣除库存
        private void updateSale(Stock stock){
            //在sql层面完成销量的+1 和版本号的 +1 并且根据商品id和版本号同时查询更新的商品
            int updateRows = stockDAO.updateSale(stock);
            if (updateRows == 0){
                throw new RuntimeException("抢购失败,请重试!!!");
            }
        }

令牌桶介绍

//创建令牌桶实例
    private RateLimiter rateLimiter = RateLimiter.create(40);

    @GetMapping("sale")
    public String sale(Integer id){
        //1.没有获取到token请求,直到获取到token令牌
//        log.info("等待时间:" +rateLimiter.acquire());
        //2.设置一个等待时间,如果在等待时间内获取到token令牌,则处理业务,若在等待时间内没有获取到响应token则抛弃
        if (!rateLimiter.tryAcquire(5, TimeUnit.SECONDS)){
            System.out.println("当前请求被限流,直接抛弃,无法调用后续秒杀逻辑...");
            return "抢购失败";
        }
        System.out.println("处理业务...............");
        return "抢购成功";
    }

令牌桶接口限流

1.导入RateLimter依赖

<!--        google开源工具类RateLimter令牌桶实现-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.2-jre</version>
        </dependency>

2.在控制类加入令牌桶功能

一次令牌桶生成20个令牌,在高并发请求中,没有拿到令牌的请求只等待两秒,若超时就抛弃请求

    //创建令牌桶实例
    private RateLimiter rateLimiter = RateLimiter.create(20);
    //乐观锁防止超卖+令牌桶限流
    @GetMapping("killtoken")
    public String killtoken(Integer id){
        System.out.println("秒杀商品的id = " + id);
        //加入令牌桶的限流措施
        if (!rateLimiter.tryAcquire(2,TimeUnit.SECONDS)){
            log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");
            return "抢购失败,当前秒杀活动过于火爆,请重试";
        }
        try {
            //根据秒杀商品id 去调用秒杀业务
            int orderId = orderService.kill(id);
            return "秒杀成功,订单id为: "+String.valueOf(orderId);
        }catch (Exception e){
            e.printStackTrace();
            return e.getMessage();
        }
    }

redis设置秒杀时间

1.导入redis依赖

<!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2.在配置文件中配置redis

spring.redis.database=0
spring.redis.port=6379
spring.redis.host=localhost

3.在校验库存之前校验redis中秒杀商品是否超时

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public int kill(Integer id) {
        //校验redis中秒杀商品是否超时
        if (!stringRedisTemplate.hasKey("kill"+id))
            throw new RuntimeException("当前商品的抢购活动已经结束啦~~");
        //校验库存
        Stock stock = checkStock(id);
        //更新库存
        updateSale(stock);
        //创建订单
        return createOrder(stock);
        }
        //校验库存
        private Stock checkStock(Integer id){
            Stock stock = stockDAO.checkStock(id);
            if (stock.getSale().equals(stock.getCount())){
                throw new RuntimeException("库存不足!!!");
            }
            return stock;
        }

接口隐藏

1.创建User实体类、dao类、mapper.xml

并在实现类导入UserDAO

@Data
public class User {
    private Integer id;
    private String name;
    private String password;
}
@Repository
public interface UserDAO {
    User findById(Integer id);
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.axiang.dao.UserDAO">
<!--    根据id查询用户-->
    <select id="findById" parameterType="Integer" resultType="User">
        select id,name,password from user where id = #{id}
    </select>
</mapper>

2.数据库建user表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS =0;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`(
`id` int(11) not null auto_increment comment '主键',
`name` varchar(80) default null comment '用户名',
`password` varchar(40) default null comment '用户密码',
primary key (`id`)
)engine = innodb auto_increment=2 default charset=utf8;

set foreign_key_checks = 1;

添加一个用户数据

3.在实现类编写getMd5方法

    @Override
    public String getMd5(Integer id, Integer userid) {
        //检验用户的合法性
        User user = userDAO.findById(userid);
        if (user == null)throw new RuntimeException("用户信息不存在!");
        log.info("用户信息:[{}]",user.toString());

        //检验商品的合法性
        Stock stock = stockDAO.checkStock(id);
        if (stock == null)throw new RuntimeException("商品信息不合法!");
        log.info("商品信息:[{}]",stock.toString());

        //生成hashKey
        String hashKey = "KEY_"+userid+"_"+id;
        //生成md5;这里的 !Q*jS# 是一个盐 随机生成
        String key = DigestUtils.md5DigestAsHex((userid+id+"!Q*jS#").getBytes());
        stringRedisTemplate.opsForValue().set(hashKey,key,120, TimeUnit.SECONDS);
        log.info("Redis写入:[{}] [{}]",hashKey,key);
        return key;
    }

4.在控制类添加生成md5的方法

    //生成md5值的方法
    @RequestMapping("md5")
    public String getMd5(Integer id,Integer userid){
        String md5;
        try{
            md5 = orderService.getMd5(id,userid);
        }catch (Exception e){
            e.printStackTrace();
            return "获取md5失败:"+e.getMessage();
        }
        return "获取md5信息为:"+md5;
    }

在这里插入图片描述

5.在实现类加入验证签名的方法

    @Override
    public int kill(Integer id, Integer userid, String md5) {
//        //校验redis中秒杀商品是否超时
//        if (!stringRedisTemplate.hasKey("kill"+id))
//            throw new RuntimeException("当前商品的抢购活动已经结束啦~~");
        //验证签名
        String hashKey = "KEY_"+userid+"_"+id;
        String s = stringRedisTemplate.opsForValue().get(hashKey);
        if (s==null) throw new RuntimeException("没有携带验证签名,请求不合法!");
        if (!s.equals(md5)) throw new RuntimeException("当前请求数据不合法,请稍后再试");

        //校验库存
        Stock stock = checkStock(id);
        //更新库存
        updateSale(stock);
        //创建订单
        return createOrder(stock);
    }

6.在控制类调用加入验证签名后的秒杀方法

    //创建令牌桶实例
    private RateLimiter rateLimiter = RateLimiter.create(20);
	//乐观锁防止超卖+令牌桶限流
    @GetMapping("killtokenmd5")
    public String killtoken(Integer id,Integer userid,String md5){
        System.out.println("秒杀商品的id = " + id);
        //加入令牌桶的限流措施
        if (!rateLimiter.tryAcquire(3,TimeUnit.SECONDS)){
            log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");
            return "抢购失败,当前秒杀活动过于火爆,请重试";
        }
        try {
            //根据秒杀商品id 去调用秒杀业务,并验证签名
            int orderId = orderService.kill(id,userid,md5);
            return "秒杀成功,订单id为: "+String.valueOf(orderId);
        }catch (Exception e){
            e.printStackTrace();
            return e.getMessage();
        }
    }

先要证明用户登录,之后再限制用户的请求频率,就可以限制接口访问压力了

在这里插入图片描述

对单个用户频率限制

在这里插入图片描述

1.编写UserService

public interface UserService {
    //向redis中写入用户访问次数
    int saveUserCount(Integer userId);
    //判断单位时间调用次数
    boolean getUserCount(Integer userId);
}

2.编写UserServiceImpl

@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public int saveUserCount(Integer userId) {
        //根据不同用户id生成调用次数的key
        String limitKey = "LIMIT"+"_"+userId;
        //获取redis中指定key的调用次数
        String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
        int limit = -1;
        if (limitNum == null){
            //第一次调用放入redis中设置为0
            stringRedisTemplate.opsForValue().set(limitKey,"0",3600, TimeUnit.SECONDS);
        }else{
            //不是第一次调用每次+1
            limit = Integer.parseInt(limitNum)+1;
            stringRedisTemplate.opsForValue().set(limitKey,String.valueOf(limit),3600,TimeUnit.SECONDS);
        }
        return limit;
    }

    @Override
    public boolean getUserCount(Integer userId) {
        //根据userId对应的key获取调用次数
        String limitKey = "LIMIT"+"_"+userId;
        //跟库用户调用次数的key获取redis中调用次数
        String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
        if (limitNum == null){
            //为空直接抛弃说明key出现异常
            log.error("该用户没有访问申请验证值记录,疑似异常");
            return true;
        }
        return Integer.parseInt(limitNum)>10;//false代表没有超过 true代表超过
    }
}

3.在控制类编写加入单用户访问频率限制

    //乐观锁防止超卖+令牌桶限流+md5签名(hash接口隐藏)+单用户访问频率限制
    @GetMapping("killtokenmd5limit")
    public String killtokenlimit(Integer id,Integer userid,String md5){
        //加入令牌桶的限流措施
        if (!rateLimiter.tryAcquire(3,TimeUnit.SECONDS)){
            log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");
            return "抢购失败,当前秒杀活动过于火爆,请重试";
        }
        try {
            //单用户调用接口的频率限制
            int count = userService.saveUserCount(userid);
            log.info("用户截至该次的访问次数为:[{}]",count);
            //进行调用次数判断
            boolean isBanned = userService.getUserCount(userid);
            if (isBanned){
                log.info("购买失败,超过频率限制!");
                return "购买失败,超过频率限制!";
            }
            //根据秒杀商品id 去调用秒杀业务,并验证签名
            int orderId = orderService.kill(id,userid,md5);
            return "秒杀成功,订单id为: "+String.valueOf(orderId);
        }catch (Exception e){
            e.printStackTrace();
            return e.getMessage();
        }
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
java实现秒杀系统@Controller @RequestMapping("seckill")//url:/模块/资源/{id}/细分 /seckill/list public class SeckillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method = RequestMethod.GET) public String list(Model model){ //获取列表页 List list=seckillService.getSeckillList(); model.addAttribute("list",list); //list.jsp+model = ModelAndView return "list";//WEB-INF/jsp/"list".jsp } @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model){ if (seckillId == null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null){ return "forward:/seckill/list"; } model.addAttribute("seckill",seckill); return "detail"; } //ajax json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){ SeckillResult result; try { Exposer exposer =seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true,exposer); } catch (Exception e) { logger.error(e.getMessage(),e); result = new SeckillResult(false,e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"} ) @ResponseBody public SeckillResult execute(@PathVariable("seckillId")Long seckillId,
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值