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、访问秒杀接口

synchronized处理并发和redis分布式锁_第1张图片
通过压测工具压测下
压测500个,线程用100个
在这里插入图片描述
synchronized处理并发和redis分布式锁_第2张图片
压测结束,就发现问题
剩余数加上下单数目,不等于总数量,超过10万份
synchronized处理并发和redis分布式锁_第3张图片

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. 只适合单点的情况,就是比如老王写的程序只能跑到单机上面,哪一天水平扩展,弄一个集群,很显然负载均衡之后,不同的用户看到的结果是不一样的

synchronized处理并发和redis分布式锁_第4张图片
synchronized处理并发和redis分布式锁_第5张图片

5、redis分布式锁

synchronized处理并发和redis分布式锁_第6张图片
synchronized处理并发和redis分布式锁_第7张图片
synchronized处理并发和redis分布式锁_第8张图片

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人
synchronized处理并发和redis分布式锁_第9张图片
压测下
在这里插入图片描述
请求发生看500个请求,但是成功下单只有5人,这就是由于没有拿到锁
synchronized处理并发和redis分布式锁_第10张图片

你可能感兴趣的:(java面试)