redis实现分布式锁

一、业务场景介绍

在Java中,关于锁我想大家都很熟悉。在并发编程中,我们通过锁,来避免由于竞争而造成的数据不一致问题,例如抢购。

private static final int TIMEOUT= 10*1000;

@Transactional
public void orderProductMockDiffUser(String productId){

    //1.查库存
    int stockNum  = stock.get(productId);
    if(stocknum == 0){
        throw new SellException(ProductStatusEnum.STOCK_EMPTY);
        //这里抛出的异常要是运行时异常,否则无法进行数据回滚,这也是spring中比较基础的   
    }else{
        //2.下单
        orders.put(KeyUtil.genUniqueKey(),productId);//生成随机用户id模拟高并发
        sotckNum = stockNum-1;
        //模拟业务处理
        try{
            Thread.sleep(100);
        } catch (InterruptedExcption e){
            e.printStackTrace();
        }
        stock.put(productId,stockNum);
    }
}

但是在高并发的情况下每次都去数据库查询显然是不合适的,因此把库存信息存入Redis中。

这里有一种比较简单的解决方案,就是synchronized关键字。

public synchronized void orderProductMockDiffUser(String productId)

这就是java自带的一种锁机制,简单的对函数加锁和释放锁。但问题是这个实在是太慢了,感兴趣的可以可以写个接口用apache ab压测一下。

ab -n 500 -c 100 http://localhost:8080/xxxxxxx

二、redis分布式锁的解决方法

首先要了解两个redis指令,SETNX 和 GETSET,可以在redis中文网上找到详细的介绍。

SETNX就是set if not exist的缩写,如果不存在就返回保存value并返回1,如果存在就返回0。
GETSET其实就是两个指令GET和SET,首先会GET到当前key的值(旧值)并返回,然后在设置当前Key为要设置Value。

新建一个redislock工具类来实现加锁和解锁:

package com.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * redis实现分布式锁
 * @author Mango
 */
@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;
        }

        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;
    }

    /**
     * 解锁
     * @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)) {
                redisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
            log.error("[redis分布式锁] 解锁异常 {}",e);
        }

    }
}

该类只有两个功能,加锁和解锁,解锁比较简单,就是删除当前key的键值对。我们主要来说一说加锁这个功能。

三、代码分析

锁的value值是当前时间加上过期时间的时间戳,之后要转成Long类型来比较早晚

首先看到用setiFAbsent方法也就是对应的SETNX,在没有线程获得锁的情况下可以直接拿到锁,并返回true也就是加锁,最后没有获得锁的线程会返回false。

然后当秒杀方法发生异常的时候,后续的线程都无法得到锁,也就陷入了一个死锁的情况。所以我们中间对于锁超时的处理。

如果没有这段代码,我们可以假设CurrentValue为A,并且在执行过程中抛出了异常,这时进入了两个value为B的线程来争夺这个锁,也就是走到了注释处currentValue=A,这时某一个线程执行到了getAndSet(key,value)函数(某一时刻一定只有一个线程执行这个方法,其他要等待)。这时oldvalue也就是之前的value等于A,在方法执行过后,oldvalue会被设置为当前的value也就是B。这时继续执行,由于oldValue=currentValue所以该线程获取到锁。而另一个线程获取的oldvalue是B,而currentValue是A,所以他就获取不到锁啦。

接下来就是在业务代码中加锁啦:首要要@Autowired注入刚刚RedisLock类,不要忘记对这个类加一个@Component注解否则无法注入

//过期时间
private static final int TIMEOUT= 10*1000;

@Transactional
public void orderProductMockDiffUser(String productId){

   long time = System.currentTimeMillions()+TIMEOUT;
   
   if(!redislock.lock(productId,String.valueOf(time)){
   		//没有获得锁
   		throw new SellException(101,"换个姿势再试试")
    }
    //1.查库存
    int stockNum  = stock.get(productId);
    if(stocknum == 0){
        throw new SellException(ProductStatusEnum.STOCK_EMPTY);
        //这里抛出的异常要是运行时异常,否则无法进行数据回滚,这也是spring中比较基础的   
    }else{
        //2.下单
        orders.put(KeyUtil.genUniqueKey(),productId);//生成随机用户id模拟高并发
        sotckNum = stockNum-1;
        try{
            Thread.sleep(100);
        } catch (InterruptedExcption e){
            e.printStackTrace();
        }
        stock.put(productId,stockNum);
    }
    //解锁
    redisLock.unlock(productId,String.valueOf(time));
}

再用apache ab压测一下。

ab -n 500 -c 100 http://localhost:8080/xxxxxxx

比synchronized快了不知道多少倍!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值