悲观锁 分布式锁 消息队列实现抢购功能

抢购功能

功能实现流程:判断用户是否已经抢购;判断是否还有商品库存;减库存;生产订单;保存购买记录;返回操作结果。

创建 SpringBoot 项目,引入 Redis 等相关依赖,配置 yml 文件

在启动时加载类中初始化抢购对象

@SpringBootApplication
public class RushApplication {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    public static void main(String[] args) {
        SpringApplication.run(RushApplication.class, args);
    }
    @Bean
    public void info(){
        // 初始化抢购对象
        stringRedisTemplate.opsForValue().set("goods:1001", "100");
    }
}

在控制器中模拟抢购流程

@RestController
@RequestMapping("/index")
public class IndexController {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    // 模拟抢购用户和商品
    Long userId = 1101L;
    Long goodsId = 1001L;
    @GetMapping("/rush")
    public String index(){
        // 判断是否有抢购标识(已成功参与抢购)
        if (stringRedisTemplate.opsForValue().get("user:"+userId+":"+goodsId) != null){
            return "已经抢购过该商品!";
        }
        // 判断是否还有库存
        Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("goods:"+goodsId));
        if (stock <= 0){
            return "抢购失败,该商品已售罄!";
        }
        // 减库存
        stock--;
        stringRedisTemplate.opsForValue().set("goods:"+goodsId, stock.toString());
        // 添加抢购标识
        stringRedisTemplate.opsForValue().set("user:"+userId+":"+goodsId, "0"); // 已购买
        return "抢购成功!";
    }
}

启动程序,初始库存为 100

在这里插入图片描述

模拟用户抢购,调用接口:http://localhost:8080/index/rush

在这里插入图片描述

库存减少,抢购标识成功添加。

浏览器显示 抢购成功!,再次调用接口显示 已经抢购过该商品!

悲观锁

userId++ 模拟多用户抢购,方法添加 synchronized 关键字实现悲观锁。

@GetMapping("/rush")
    public synchronized String index(){
        userId++;
        // ......
    }
}

使用 apache-jmeter 压力测试工具,线程组中线程属性设置如下。

在这里插入图片描述

设置 HTTP 请求,添加路径 http://localhost:8080/index/rush

运行查看结果,100个模拟用户抢购完全部库存,没有出现错误。

在这里插入图片描述

分布式锁

RedisUtil Redis工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedisTemplate<Object, Object> redisTemplate;
    @Resource(name = "stringRedisTemplate")
    ValueOperations<String, String> valOpsStr;
    @Resource(name = "redisTemplate")
    ValueOperations<Object, Object> valOpsObj;
    /**
     * 根据指定key获取String
     * @param key
     * @return
     */
    public String getStr(String key){
        return valOpsStr.get(key);
    }
    /**
     * 设置Increment缓存
     * @param key
     */
    public long getIncrement(String key){

        return valOpsStr.increment(key);
    }
    /**
     * 设置Increment缓存
     * @param key
     * @param val
     */
    public long setIncrement(String key, long val){

       return valOpsStr.increment(key,val);
    }
    /**
     * 设置Str缓存
     * @param key
     * @param val
     */
    public void setStr(String key, String val){
        valOpsStr.set(key,val);
    }
    /***
     * 设置Str缓存
     * @param key
     * @param val
     * @param expire 超时时间
     */
    public void setStr(String key, String val,Long expire){
        valOpsStr.set(key,val,expire, TimeUnit.MINUTES);
    }
    /**
     * 删除指定key
     * @param key
     */
    public void del(String key){
        stringRedisTemplate.delete(key);
    }
    /**
     * 根据指定o获取Object
     * @param o
     * @return
     */
    public Object getObj(Object o){
        return valOpsObj.get(o);
    }
    /**
     * 设置obj缓存
     * @param o1
     * @param o2
     */
    public void setObj(Object o1, Object o2){
        valOpsObj.set(o1, o2);
    }
    /**
     * 删除Obj缓存
     * @param o
     */
    public void delObj(Object o){
        redisTemplate.delete(o);
    }
    /***
     * 加锁的方法
     * @return
     */
    public boolean lock(String key,Long expire){
       RedisConnection redisConnection=redisTemplate.getConnectionFactory().getConnection();
       //设置序列化方法
       redisTemplate.setKeySerializer(new StringRedisSerializer());
       redisTemplate.setValueSerializer(new StringRedisSerializer());

       if(redisConnection.setNX(key.getBytes(),new byte[]{1})){
           redisTemplate.expire(key,expire,TimeUnit.SECONDS);
           redisConnection.close();
           return true;
       }else{
           redisConnection.close();
           return false;
       }
    }
    /***
     * 解锁的方法
     * @param key
     */
    public void unLock(String key){
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.delete(key);
    }
}

SETNX 是 Redis 数据库中的一个命令,它是 “SET if Not eXists” 的缩写。这个命令的作用是设置一个键值对,但前提是这个键(key)在数据库中尚未存在。如果键已经存在,SETNX 命令不会执行任何操作,并且返回 0;如果键不存在,则会设置键值对,并返回 1。

基本语法:

SETNX key value
  • key:想要设置的键。
  • value:想要关联到键的值。

SETNX 命令经常用于实现分布式锁,因为它的原子性操作可以确保多个客户端不会同时设置同一个键。

使用工具类中的加锁解锁方法:

// 加锁
redisUtil.lock(luckKey,expire)
// 解锁(释放锁)
redisUtil.unLock(luckKey);

修改过后的 Controller

@RestController
@RequestMapping("/index")
public class IndexController {
    @Resource
    private RedisUtil redisUtil;
    // 模拟抢购用户和商品
    Long userId = 1101L;
    Long goodsId = 1001L;
    @GetMapping("/rush")
    public synchronized String index(){
        userId++;
        while (!redisUtil.lock("lock_" + goodsId,2L)){
            // 休眠
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String msg = "";
        // 判断是否有抢购标识(未成功参与抢购)
        if (redisUtil.getStr("user:"+userId+":"+goodsId) == null){
            // 判断是否还有库存
            Integer stock = Integer.parseInt(redisUtil.getStr("goods:"+goodsId));
            if (stock <= 0){
                msg = "抢购失败,该商品已售罄!";
            } else {
                // 减库存
                stock--;
                redisUtil.setStr("goods:"+goodsId, stock.toString());
                // 添加抢购标识
                redisUtil.setStr("user:"+userId+":"+goodsId, "0"); // 已购买
                msg = "抢购成功!";
            }
        } else { // (成功参与抢购)
            msg = "已经抢购过该商品!";
        }
        // 释放锁
        redisUtil.unLock("lock_" + goodsId);
        return msg;
    }
}

测试

在这里插入图片描述

消息队列

引入相关依赖,配置 yml 文件

<!-- rabbitmq -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
spring:
  redis:
    password: 123456
    host: 127.0.0.1
    port: 6379
    database: 1
  rabbitmq:
    port: 5672
    host: 127.0.0.1
    listener:
      simple:
        acknowledge-mode: manual # 手动反馈
        prefetch: 1 # 限流
        concurrency: 1 # 并发数(消费者数量)
        max-concurrency: 1 # 最大并发数(最大消费者数量)
        auto-startup: true # 是否自动启动

启动时创建队列

@Configuration
public class RabbitMQConfig {
    public static final String QUEUE_NAME = "test_qg";
    @Bean
    public Queue queue(){
        return QueueBuilder.durable(QUEUE_NAME)
                // 设置队列最大长度
                .maxLength(50)
                // 队列溢出后,拒绝消息
                .overflow(QueueBuilder.Overflow.rejectPublish)
                .build();
    }
}

用户抢购商品类

@Data
@ToString
public class UserGoods implements Serializable {
    private Integer userId;
    private String goodsId;
}

监听器

@Component
public class RabbitMQListener {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void receiveMessage(Channel channel, Message message) {
        HashOperations hashOperations =  stringRedisTemplate.opsForHash();
        ValueOperations valueOperations = stringRedisTemplate.opsForValue();
        try {
            // 反序列化 将字节数组转为对象
            UserGoods userGoods = (UserGoods) SerializationUtils.deserialize(message.getBody());
            //判断是否还有库存  并且已经购买过
            if(!(hashOperations.hasKey(userGoods.getGoodsId()+":success",userGoods.getUserId().toString()))
                    &&Integer.valueOf(valueOperations.get("goods:"+userGoods.getGoodsId()).toString())>0){
                //减库存
                valueOperations.increment("goods:"+userGoods.getGoodsId(),-1);
                //添加抢购标识
                hashOperations.put(userGoods.getGoodsId()+":success",userGoods.getUserId().toString(),"0");//抢到了
            }else{
                hashOperations.put(userGoods.getGoodsId()+":error",userGoods.getUserId().toString(),"1");
            }
            // 手动反馈
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e){
            // 放回队列
            try {
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            } catch (IOException ex) {
                e.printStackTrace();
            }
            e.printStackTrace();
        }
    }
}

控制器

@RestController
@RequestMapping("/rabbit")
public class RabbitmqController {
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 模拟抢购用户和商品
    Integer userId = 1100;
    String goodsId = "1001";
    @GetMapping("/index")
    public  String index(){
        userId++;
        UserGoods userGoods = new UserGoods();
        userGoods.setUserId(userId);
        userGoods.setGoodsId(goodsId);
        rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME,userGoods);
        return "用户"+userId+"正在抢购"+goodsId;
    }

    @GetMapping("/refund")
    public String refund(Integer userId,String goodsId){
        HashOperations hashOperations = stringRedisTemplate.opsForHash();
        if(hashOperations.hasKey(goodsId+":success",userId.toString())){
            return "抢购成功!";
        }else if(hashOperations.hasKey(goodsId+":error",userId.toString())){
            return "抢购失败!";
        }else {
            return "等待";
        }
    }
}

运行程序,去到 http://127.0.0.1:15672/

在这里插入图片描述

队列 test_qg 创建成功

使用 jmeter 测试高并发下接口性能

在这里插入图片描述

结果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值