前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis使用场景一(活动秒杀)

Redis使用场景一(活动秒杀)

原创
作者头像
憨批程序员
修改2020-05-06 17:00:06
1.7K0
修改2020-05-06 17:00:06
举报
文章被收录于专栏:小小程序员

场景一:活动秒杀

redis的互斥锁可以解决这个问题,redis的setnx命令在指定的 key 不存在时,为 key 设置指定的值。当存在时,则无法插入值

redis Setnx 命令基本语法如下:

代码语言:javascript
复制
redis 127.0.0.1:6379> SETNX KEY_NAME VALUE

可用版本 >= 1.0.0

返回值 设置成功,返回 1 。 设置失败,返回 0 。

对应java的RedisTemplate用法

代码语言:java
复制
//设置key对应的值,如果存在返回false,否则返回true
redisTemplate.opsForValue().setIfAbsent(key, value)

具体的可业务代码实现参考(转载自 简书分布式锁--Redis秒杀(互斥锁)

一. 分布式锁SetNX实现

1.redis 分布式锁

RedisLock.java

代码语言:javascript
复制
/**
 * 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. 业务模拟实现
业务接口
代码语言:javascript
复制
public interface SeckillService {

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

    /**
     * 秒杀的逻辑方法
     * @param productId
     */
    void orderProductMocckDiffUser(String productId);
}
3.业务实现
代码语言:javascript
复制
@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));
    }
}

4.工具类

SellException .java

代码语言:javascript
复制
/**
 * 自定义异常
 */
@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

代码语言:javascript
复制
@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

代码语言:javascript
复制
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);
    }
}

二:使用INCR实现

代码语言:javascript
复制
@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. 如果有一个业务操作耗时会超过设定的过期时间。 需要使用后台线程进监控操作,不断的去监控锁,如果该线程还存在,就给锁续期。也就是该锁必须有该线程进行解锁。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 场景一:活动秒杀
  • 一. 分布式锁SetNX实现
    • 2. 业务模拟实现
    • 二:使用INCR实现
    • 三、互斥锁存在的问题
    相关产品与服务
    云数据库 Redis
    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档