使用redis实现分布式锁

模拟秒杀代码

import com.wechatorder.sell.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/skill")
@Slf4j
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

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

    /**
     * 秒杀 抢到了会返回剩余库存量
     * @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);
    }
}
public interface SeckillService {

    String querySeckillProductInfo(String productId);

    void orderProductMockDiffUser(String productId);
}
@Service
public class SeckillServiceImpl implements SeckillService {
    /**
     * 活动皮蛋粥特价 限量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);
    }

    @Override
    public void orderProductMockDiffUser(String productId) {
        //查询该商品库存,为0则活动结束.
        int stockNum=stock.get(productId);
        if (stockNum==0){
            throw new SellException(100,"活动结束");
        }else {
            //下单(模拟不同用户openid不同)
            orders.put(KeyUtil.genUniqueKey(),productId);
            //减库存
            stockNum=stockNum-1;
            try {
                Thread.sleep(100);
            }catch (Exception e){
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }
    }
}

这里减库存的代码如果在高并发环境下会出现问题

使用Apache ab压测模拟并发

ab -n 100 -c 10 http://127.0.0.1:8080/sell/skill/order/123456

-n 50 表示表示发出100个请求

-c 10 表示10个并发数
使用redis实现分布式锁
使用redis实现分布式锁

这时发现并没有出现问题

然后执行

ab -n 500 -c 100 http://127.0.0.1:8080/sell/skill/order/123456
使用redis实现分布式锁

这时就产生了超卖的现象

很多学过多线程的小伙伴肯定会说使用synchronized关键字加锁就可以解决了

代码如下

@Override
public synchronized void orderProductMockDiffUser(String productId) {
    //查询该商品库存,为0则活动结束.
    int stockNum=stock.get(productId);
    if (stockNum==0){
        throw new SellException(100,"活动结束");
    }else {
        //下单(模拟不同用户openid不同)
        orders.put(KeyUtil.genUniqueKey(),productId);
        //减库存
        stockNum=stockNum-1;
        try {
            Thread.sleep(100);
        }catch (Exception e){
            e.printStackTrace();
        }
        stock.put(productId,stockNum);
    }
}

然后我们再次执行

ab -n 500 -c 100 http://127.0.0.1:8080/sell/skill/order/123456
使用redis实现分布式锁
这次没有出现问题 但是速度明显的慢了很多

synchronized的确是一种解决办法,但是无法做到细粒度的控制,而且只适合单点的情况

我们刚刚是把他加到一个方法上,假如我们有很多个商品,每个商品的id不一样 但是他们都要访问这个方法,假如秒杀A商品的人很多,秒杀B商品的人很少,一旦进入这个方法都会造成一样的慢.

那有没有其他的办法呢

可以使用基于redis的分布式锁来解决

我们先来了解两个redis指令

SETNX key value

**时间复杂度:**O(1)

key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。

返回值

Integer reply, 特定值:

  • 1 如果key被设置了
  • 0 如果key没有被设置

例子

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> 

GETSET key value

**时间复杂度:**O(1)

自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误。

返回值

bulk-string-reply: 返回之前的旧值,如果之前Key不存在将返回nil

例子

redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
redis> 

代码实现

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.simpleframework.xml.core.Commit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key,String value){
        if (redisTemplate.opsForValue().setIfAbsent(key,value)){
            return true;
        }
        //currentValue=A  这两个线程的value都是B 只会让其中一个线程拿到锁
        String currentValue =redisTemplate.opsForValue().get(key);
        //如果锁过期
        if(!StringUtils.isEmpty(currentValue)&&Long.parseLong(currentValue)<System.currentTimeMillis()){
            //获取上一个锁的时间
            String oldValue=redisTemplate.opsForValue().getAndSet(key,value);
            if (!StringUtils.isEmpty(oldValue)&&oldValue.equals(currentValue)){
                return true;
            }
        }
        return false;
    }

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

    }
}
import com.wechatorder.sell.exception.SellException;
import com.wechatorder.sell.service.RedisLock;
import com.wechatorder.sell.service.SeckillService;
import com.wechatorder.sell.utils.KeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.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);
    }

    @Override
    public  void orderProductMockDiffUser(String productId) {
        //加锁
        long time=System.currentTimeMillis()+TIMEOUT;
        if (!redisLock.lock(productId,String.valueOf(time))){
            throw new SellException(101,"人也太多了,换个姿势再试试~");
        }
        //查询该商品库存,为0则活动结束.
        int stockNum=stock.get(productId);
        if (stockNum==0){
            throw new SellException(100,"活动结束");
        }else {
            //下单(模拟不同用户openid不同)
            orders.put(KeyUtil.genUniqueKey(),productId);
            //减库存
            stockNum=stockNum-1;
            try {
                Thread.sleep(100);
            }catch (Exception e){
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }

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

这时我们再来测试一次
使用redis实现分布式锁
显示只有5个人 但是总数是对的上的,说明只有5人拿到了锁

redis实现分布式锁的优点:

支持分布式

可以更细粒度的控制

分布式锁可以概括为:

多台机器上多个进程对一个数据进行操作的互斥

我们利用了setnx和getset命令这种特点实现了分布式锁.redis适合做分布式锁一个很重要的原因其实是因为redis是单线程的