分布式锁--Redis秒杀(互斥锁)(一)

本文通过一个中秋月饼秒杀案例,介绍了如何使用Redis的SetNX和GetSet命令实现分布式锁来处理并发问题。业务实现包括查询特价商品、模拟秒杀逻辑,并展示了基于Redis INCR命令的简化实现。同时,文章讨论了分布式锁存在的问题,如异常处理导致的解锁缺失,并提出了解决方案,如设置锁的过期时间和使用finally块确保解锁。并发测试结果显示了不同锁策略的影响。
摘要由CSDN通过智能技术生成

一、业务场景

中秋佳节,进行月饼秒杀,特价,限量1000份,不限每人秒的份数,不要超卖即可。

二、SetNX实现

1. 分布式锁SetNX实现

RedisLock.java

/**
 * redis 分布式锁
 */
@Component
@Slf4j
public class RedisLock {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key productId - 商品的唯一标志
     * @param value  当前时间+超时时间
     * @return
     */
    public boolean lock(String key,String value){
        if(redisTemplate.opsForValue().setIfAbsent(key,value)){//对应setnx命令
            //可以成功设置,也就是key不存在
            return true;
        }
        //判断锁超时 - 防止原来的操作异常,没有运行解锁操作  防止死锁
        String currentValue = (String) redisTemplate.opsForValue().get(key);
        //如果锁过期
        if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){//currentValue不为空且小于当前时间
            //获取上一个锁的时间value
            String oldValue = (String)redisTemplate.opsForValue().getAndSet(key,value);//对应getset,如果key存在
            //假设两个线程同时进来,key被占用了。获取的值currentValue=A(get取的旧的值肯定是一样的),两个线程的value都是B,key都是K.锁时间已经过期了。
            //而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的value已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
            if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
                //oldValue不为空且oldValue等于currentValue,也就是校验是不是上个对应的商品时间戳,也是防止并发
                return true;
            }
        }
        //无锁
        return false;
    }
    /**
     * 解锁
     * @param key
     * @param value
     */
    public void unlock(String key,String value){
        try {
            String currentValue = (String)redisTemplate.opsForValue().get(key);
            if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value) ){
                redisTemplate.opsForValue().getOperations().delete(key);//删除key
            }
        } catch (Exception e) {
            log.error("[Redis分布式锁] 解锁出现异常了,{}",e);
        }
    }
}

2. 业务模拟实现

1) 业务接口

public interface SeckillService {

    /**
     * 查询特价商品
     * @param productId
     * @return
     */
    String querySecKillProductInfo(String productId);

    /**
     * 秒杀的逻辑方法
     * @param productId
     */
    void orderProductMocckDiffUser(String productId);
}

3) 业务实现

@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private RedisLock redisLock;

    private static final int TIMEOUT = 10*1000;//超时时间 10s

    /**
     * 活动,特价,限量100000份
     */
    static Map<String,Integer> products;//模拟商品信息表
    static Map<String,Integer> stock;//模拟库存表
    static Map<String,String> orders;//模拟下单成功用户表
    static {
        /**
         * 模拟多个表,商品信息表,库存表,秒杀成功订单表
         */
        products = new HashMap<>();
        stock = new HashMap<>();
        orders = new HashMap<>();
        //其中666666为单号
        products.put("666666",1000);
        stock.put("666666",1000);
    }

    private String queryMap(String productId){//模拟查询数据库
        return "中秋活动,月饼特卖,限量"
                +products.get(productId)
                +"份,还剩:"+stock.get(productId)
                +"份,该商品成功下单用户数:"
                +orders.size()+"人";
    }

    @Override
    public String querySecKillProductInfo(String productId) {
        return this.queryMap(productId);
    }

    //解决方法二,基于Redis的分布式锁
    // http://redis.cn/commands/setnx.html
    // http://redis.cn/commands/getset.html
    // SETNX命令  将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做
    // GETSET命令  先查询出原来的值,值不存在就返回nil。然后再设置值
    //支持分布式,可以更细粒度的控制
    //多台机器上多个线程对一个数据进行操作的互斥。
    //Redis是单线程的!!!
    @Override
    public void orderProductMocckDiffUser(String productId) {
        //解决方法一:synchronized锁方法是可以解决的,但是请求会变慢,请求变慢是正常的。
        // 主要是没做到细粒度控制。比如有很多商品的秒杀,但是这个把所有商品的秒杀都锁住了。
        // 而且这个只适合单机的情况,不适合集群

        //加锁
        long time = System.currentTimeMillis() + TIMEOUT;
        if(!redisLock.lock(productId,String.valueOf(time))){
            throw new SellException(101,"很抱歉,人太多了,换个姿势再试试~~");
        }
        //1.根所单号666666,查询该商品库存,为0则活动结束
        int stockNum = stock.get(productId);
        if(stockNum==0){
            throw new SellException(100,"活动结束");
        }else {
            //2.下单 key为生成单号,值为 商品ID也就是 666666
            orders.put(KeyUtil.getUniqueKey(),productId);
            //3.下单成功,减库存
            stockNum =stockNum-1;
            // 不做处理的话,高并发下会出现超卖的情况,下单数,
            // 大于减库存的情况。虽然这里减了,但由于并发,减的库存还没存到map中去。
            // 新的并发拿到的是原来的库存
            try{
                Thread.sleep(100);//模拟减库存的处理时间
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            //4. 商品更新库存
            stock.put(productId,stockNum);
        }

        //解锁
        redisLock.unlock(productId,String.valueOf(time));
    }
}
  1. 工具类
    SellException .java
/**
 * 自定义异常
 */
@Data
public class SellException extends RuntimeException{
    private Integer code;
    public SellException(ResultEnum resultEnum) {
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }
    public SellException(Integer code, String defaultMessage) {
        super(defaultMessage);
        this.code=code;
    }
}

ResultEnum.java

@Getter
public enum ResultEnum {
    //消息枚举
    SUCCESS(0,"成功"),
    PARAM_ERROR(1,"参数不正确")
    ;

    private Integer code;
    private String message;

    ResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

KeyUtil.java

public class KeyUtil {
    /**
     * 生成唯一主键
     * 格式:时间+随机数
     * @return
     */
    public static synchronized String getUniqueKey(){//加一个锁
        Random random = new Random();
        Integer number = random.nextInt(900000) + 100000;//随机六位数
        return System.currentTimeMillis()+String.valueOf(number);
    }
}
  1. 测试
    商品查询地址:http://localhost:8081/skill/query/666666
    秒杀下单地址:http://localhost:8081/skill/order/666666

商品查询

秒杀下单

  1. 并发测试

三、使用INCR实现

@RestController
@Slf4j
@RequestMapping(value = "/secondSkill", produces = "application/json;charset=UTF-8")
public class SecondSkillController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping(value = "/initData")
    public String initData() {
        redisTemplate.opsForValue().set("stock", 100);
        redisTemplate.opsForValue().set("count", 0);
        return "商品库存数量为:" + redisTemplate.opsForValue().get("stock") + "抢到商品人数为:"
                + redisTemplate.opsForValue().get("count");
    }

    @GetMapping("/secondSkill")
    public String secondSkill() {
        String stock = (String) redisTemplate.opsForValue().get("stock");
        String count = (String) redisTemplate.opsForValue().get("count");
        int pCount = Integer.valueOf(count);
        int stockNum = Integer.valueOf(stock);
//        Long lock = redisTemplate.opsForValue().increment("lock", 1);
        //应为1
//        if (lock == 1) {
//            redisTemplate.expire("lock",10L, TimeUnit.SECONDS);
            if (stockNum > 0) {
                try {
                    pCount++;
                    redisTemplate.opsForValue().set("count", pCount);
                    log.info("库存数量为" + stockNum);
                    //业务逻辑
                    stockNum--;
                    //更改库存
                    redisTemplate.opsForValue().set("stock", stockNum);
                } catch (Exception e) {
                    //解锁
                    redisTemplate.opsForValue().increment("lock", -1);
                }
            } else {
                return "库存不足";
            }
//        }
        return "商品库存剩余数量为:" + stockNum + "-》" + redisTemplate.opsForValue().get("count") + "人成功购买1件商品";
    }

    @GetMapping(value = "/queryStock")
    public String queryStock() {
        return "剩余商品库存数量为:" + redisTemplate.opsForValue().get("stock") + "-》" + redisTemplate.opsForValue().get("count") + "人成功购买1件商品";
    }
}

  1. 测试
    初始化数据:
    http://localhost:8081/secondSkill/queryStock/initData
    查看库存:
    http://localhost:8081/secondSkill/queryStock
    秒杀地址:
    http://localhost:8081/secondSkill/secondSkill

  2. 并发测试结果
    1)没有加锁

2)无解锁

 

3)解锁

四、互斥锁存在的问题

  1. 如果业务中出现问题,出异常不没有执行到解锁。
    解决方案:
    在finally中添加解锁操作。
  2. 如果在集群中有一台机子抢到锁,但宕机了。
    解决方案:
    添加过期时间。
  3. 如果有一个业务操作耗时会超过设定的过期时间。
    需要使用后台线程进监控操作,不断的去监控锁,如果该线程还存在,就给锁续期。也就是该锁必须有该线程进行解锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值