synchronized处理并发和redis分布式锁

1、控制层

@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {
    @Autowired
    private SecKillService secKillService;

    /**
     * 查询秒杀活动特价商品的信息
     * @param productId
     * @return
     */
    @GetMapping("/query/{productId}")
    public String query(@PathVariable String productId)throws Exception
    {
        return secKillService.querySecKillProductInfo(productId);
    }


    /**
     * 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
     * @param productId
     * @return
     * @throws Exception
     */
    @GetMapping("/order/{productId}")
    public String skill(@PathVariable String productId)throws Exception
    {
        log.info("@skill request, productId:" + productId);
        secKillService.orderProductMockDiffUser(productId);
        return secKillService.querySecKillProductInfo(productId);
    }
}

2、service层

去到SecKillServiceImpl里面的orderProductMockDiffUser看下怎么写的
这里用到3个map,分别模拟了 三个表,
有商品的信息,有库存,有订单

@Service
public class SecKillServiceImpl implements SecKillService {
    private static final int TIMEOUT = 10 * 1000; //超时时间 10s

    @Autowired
    private RedisLock redisLock;

    /**
     * 国庆活动,皮蛋粥特价,限量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<>();
        //秒杀订单,一万件
        products.put("123456", 100000);
        stock.put("123456", 100000);
    }

    /**查询方法,返回总共多少,还剩余多少,多少个人下单**/
    private String queryMap(String productId)
    {
        return "国庆活动,皮蛋粥特价,限量份"
                + products.get(productId)
                +" 还剩:" + stock.get(productId)+" 份"
                +" 该商品成功下单用户数目:"
                +  orders.size() +" 人" ;
    }

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

    // TODO: redis分布式解锁加锁
    /**主要秒杀的逻辑方法**/
    @Override
    public void orderProductMockDiffUser(String productId)
    {
        //1.首先查询该商品库存,为0则活动结束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活动结束");
        }else {
            //2.下单(模拟不同用户openid不同)
            orders.put(KeyUtil.genUniqueKey(),productId);
            //3.减库存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }
    }
}

3、访问秒杀接口

在这里插入图片描述
通过压测工具压测下
压测500个,线程用100个
在这里插入图片描述
在这里插入图片描述
压测结束,就发现问题
剩余数加上下单数目,不等于总数量,超过10万份
在这里插入图片描述

4、看下代码

很多请求来的时候,大家都在下单
在sleep过程中,会有很多用户在下单,这就会造成下单数量,大于减库存数量,会多出很多
可能可以说,这里是从map里面查的,所以会出这个问题,如果直接从数据库里面查,select语句加上for update这样就能锁住,就不会出现这个问题。
但是很多东西不放在数据库,就是由于内存,redis这些查询比数据库快很多
假如我们现在需求就是要放在内存中,不能放在数据库。
可以加上synchronized关键字。

/**主要秒杀的逻辑方法**/
@Override
public synchronized void orderProductMockDiffUser(String productId)
{
    //1.首先查询该商品库存,为0则活动结束。
    int stockNum = stock.get(productId);
    if(stockNum == 0) {
        throw new SellException(100,"活动结束");
    }else {
        //2.下单(模拟不同用户openid不同)
        orders.put(KeyUtil.genUniqueKey(),productId);
        //3.减库存
        stockNum =stockNum-1;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stock.put(productId,stockNum);
    }
}

重启,在压测
发现压测越来越慢
synchronized关键字,就是用一个方法将它锁住了,而每次访问这个方法的线程,只会有一个线程,所以就是导致它慢的原因
通过这种方法在保证,这个里面的方法是单线程来处理,不会出现什么问题

synchronized关键字总结

  1. 无法做到细粒度的控制,所以无论人员多少,方法都是一样的慢
  2. 只适合单点的情况,就是比如老王写的程序只能跑到单机上面,哪一天水平扩展,弄一个集群,很显然负载均衡之后,不同的用户看到的结果是不一样的

在这里插入图片描述
在这里插入图片描述

5、redis分布式锁

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.1 下面使用redis来加锁和解锁

解决死锁和在多个线程同时访问进来的时候,只会让一个线程拿到锁

@Component
@Slf4j
public class RedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key, String value) {
        //用到setnx命令,java里面不一样setIfAbsent方法,返回Boolean
        //如果setIfAbsent就是被锁定了
        if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
        /**
         * 如果没有下面的步骤,就会直接返回false,
         * 会直接造成死锁的情况,
         * 因为设置了过期时间,
         * 就是里面的值小于当前时间的话,进到下面的代码中,就会返回ture
         * 返回true,就会解开了那个死锁,即可继续进行下去,而不会一直被锁住
         * 还解决一个问题
         * 就是在多个线程同时访问进来的时候,只会让一个线程拿到锁
         * **/
        //如果锁超时,就判断一下,当前的值从redis里面获取key的值
        //currentValue=A   这两个线程的value都是B  只会其中一个线程拿到锁
        String currentValue = redisTemplate.opsForValue().get(key);
        //如果锁过期
        //多个线程进来的时候,只会一个线程拿到锁
        //就是存储进去的时间小于当前时间
        if (!StringUtils.isEmpty(currentValue)
                && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间
            //使用getAndSet方法,此时value值是B
            //第一个线程拿到oldvalue值就是A,当前的currentValue也是A,如果相等就返回ture
//            第二个线程拿到oldvalue值是B,但是currentValue一直是A,不相等,就没有拿到这把锁
            String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
            //如果这个值不为空,并且和当前的value值相等的话
            if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 解锁
     * @param key
     * @param value
     */
    public void unlock(String key, String value) {
        try {
            String currentValue = redisTemplate.opsForValue().get(key);
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
                //解锁就是删掉key
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        }catch (Exception e) {
            log.error("【redis分布式锁】解锁异常, {}", e);
        }
    }
}

5.2 回到实现接口SecKillServiceImpl

// TODO: redis分布式解锁加锁
    /**主要秒杀的逻辑方法**/
    @Override
    public void orderProductMockDiffUser(String productId)
    {
        //加锁
        long time = System.currentTimeMillis() + TIMEOUT;
        //返回是布尔类型,判断是否加锁成功
        if (!redisLock.lock(productId,String.valueOf(time))){
            throw new SellException(101,"哎呦喂,人也太多了,换个姿势再进来试试。。。");
        }

        //保证下面代码单线程访问
        //1.首先查询该商品库存,为0则活动结束。
        int stockNum = stock.get(productId);
        if(stockNum == 0) {
            throw new SellException(100,"活动结束");
        }else {
            //2.下单(模拟不同用户openid不同)
            orders.put(KeyUtil.genUniqueKey(),productId);
            //3.减库存
            stockNum =stockNum-1;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }

        //解锁
        redisLock.unlock(productId,String.valueOf(time));

    }

5.3 运行,压测

刚开始为0人
在这里插入图片描述
压测下
在这里插入图片描述
请求发生看500个请求,但是成功下单只有5人,这就是由于没有拿到锁
在这里插入图片描述

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
悲观锁是一种并发控制机制,它假设并发访问的操作会导致冲突,因此在操作开始前就会锁定资源,阻止其他并发操作的访问。悲观锁在实现上通常使用数据库锁或者线程锁的方式来实现,比如使用数据库的行锁或者select...for update来锁定资源,或者使用synchronized关键字来锁定线程。 版本号机制是一种乐观并发控制机制,它假设并发访问的操作不会导致冲突,因此允许同时进行并发操作。版本号机制通常使用一个版本号字段来记录资源的版本信息,每次操作时都会比较该版本号与当前版本号是否一致,如果一致则进行操作,同时将版本号加1,如果不一致则认为资源已经被其他并发操作修改,操作失败。 Redis分布式锁是使用Redis实现的一种分布式锁机制。它通过在Redis中存储一个特定的Key-Value对来实现对资源的锁定和解锁。在进行锁定时,通过对特定的Key设置一个过期时间来避免锁的过长时间占用;在进行解锁时,通过对特定的Key删除操作来释放资源。Redis分布式锁通常使用SETNX命令来尝试获取锁,并使用DEL命令来释放锁。 悲观锁和版本号机制都是在操作开始前就进行了并发控制,因此在性能上可能存在一定的开销;而Redis分布式锁由于基于内存数据库Redis实现,具有高效的读写速度和高可用性,适合用于分布式环境下的并发控制。 在具体实践中,选择悲观锁还是版本号机制还是Redis分布式锁,需要根据具体应用场景和需求来决定。如果资源冲突较为频繁,可以考虑使用悲观锁;如果资源冲突较少,可使用版本号机制;如果需要在分布式环境下控制并发访问,可以选择Redis分布式锁

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值