Redis分布式锁

65 阅读6分钟

环境搭建

properties

server.port=1111
spring.redis.database=0
spring.redis.host=192.168.56.10
spring.redis.port=6379
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默认0
spring.redis.lettuce.pool.min-idle=0

redis的相关配置。

pom.xml

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>


        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

config

/**
 * @author kylin
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();

        //序列化配置
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

设置redis的key、value序列化配置。

controller

@RestController
public class GoodController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() {
        //get key 查看库存的数量够不够
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if (goodsNumber > 0) {
            //库存数量-1
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
            System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
            return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
    }
}

创建两个redis项目,一个在端口1111运行,一个在2222运行。(redis记得启动)

redis中给键名为goods:001设置值value为100

启动项目访问http://localhost:1111/buyGoodshttp://localhost:3333/buyGoods

消费商品成功~

1.0

@GetMapping("/buyGoods")
public String buyGoods() {
    //get key 查看库存的数量够不够
    String result = stringRedisTemplate.opsForValue().get("goods:001");
    int goodsNumber = result == null ? 0 : Integer.parseInt(result);
    if (goodsNumber > 0) {
        //库存数量-1
        int realNumber = goodsNumber - 1;
        stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
        System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
        return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
    }
    return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}

此时这段代码,在一个线程是没有问题的,但是多线程下,则会出现各种问题,所以需要加锁。

2.0(加锁)

加锁的话我们是使用synchronized还是lock呢??

@RestController
public class GoodController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() {
        synchronized (this) {
            //get key 查看库存的数量够不够
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                //库存数量-1
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        }
    }
}

如果使用synchronized,像上图这样,虽然成功加锁,但是其他的请求线程则会一直停在这等待,锁的释放,请求会一直在转圈,造成线程的挤压。

synchronizedlock的区别是在与业务。

  • 不见不散
  • 过时不候

lock可以设置尝试获取时间,超过了则做其他操作。

synchronized则一直等待。

所以我们可以使用locktryLock()方法,设置获取时间,超过了则做其他操作。

配置nginx

docker run --name nginx -p 80:80 -d nginx

incloude /etc/nginx/conf.d/*.conf注释,否则会默认先加载这个文件下的conf配置。导致下面我们配的失效

访问http://192.168.56.10/buyGoods/则会负载均衡到本地启动的两个项目中

此时我们访问http://192.168.56.10/buyGoods/则会轮询消费1111和3333项目了。

这样子我们这个单机版下好像解决了锁的问题(本地锁),但是分布式下是锁不住的,因为如果有10个这样的项目,每个项目同时都只有一个线程能运行,那么10个项目则会有10个线程去操作资源,这样还是多线程,会产生线程问题的!

我们可以使用JMeter进行验证

点击运行之后,我们查看项目日志

可以发现出现了多个商品,卖出去多次的情况,这样显然是不合理的!

所以我们则需要去一个统一的地方去管理,像redis、zookeeper、mysql

为了解决这种情况,我们则需要分布式锁,选择redis,也就是redis分布式锁

3.0(redis分布式锁)

我们使用redis的set命令进行操作

 public final String REDIS_LOCK = "REDIS_LOCK";  
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
/**
 * @author kylin
 */
@RestController
public class GoodController {

    public final String REDIS_LOCK = "REDIS_LOCK";

    @Autowired
    StringRedisTemplate stringRedisTemplate;


    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
        //如果已经存在
        if (!flag) {
            return "抢锁失败!";
        }
        synchronized (this) {
            //get key 查看库存的数量够不够
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                //库存数量-1
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                //解锁
                stringRedisTemplate.delete(REDIS_LOCK);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            }
        }
        return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
    }
}

思路其实很简单,首先获取到锁的对象,会在reidis中创建一个键名为REDIS_LOCK的对象,给其设置一个随机值。随后进行操作资源,操作完成后在redis中删除该对象stringRedisTemplate.delete(REDIS_LOCK);

而后面的线程也会进行其操作通过setIfAbsent(),只有redis中没有键名为REDIS_LOCK的对象时才能设置成功,如果redis中已经存在,说明已经有线程获取到了锁,并且没有释放。设置失败则return结束。

是否还有其他问题出现呢??

加入获取锁的线程再运行中出现了异常,导致程序没有继续执行下去,从而没有把redis中的REDIS_LOCK给删除,那么后面的其他请求则都不会成功运行!

4.0(finaly)

/**
 * @author kylin
 */
@RestController
public class GoodController {

    public final String REDIS_LOCK = "REDIS_LOCK";

    @Autowired
    StringRedisTemplate stringRedisTemplate;


    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() {
        try {
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
            //如果已经存在
            if (!flag) {
                return "抢锁失败!";
            }
            synchronized (this) {
                //get key 查看库存的数量够不够
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);
                if (goodsNumber > 0) {
                    //库存数量-1
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                }
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        } finally {
            //解锁
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }
}

将从redis删除对象的操作写在finally代码快中,保证最后一定能释放。

(使用的是synchronized出现异常,jvm会自动释放锁,如果使用的是Lock,则还需要在finally代码快中加入unlock操作释放锁)

是否还存在着问题呢???

上面我们假设的是程序出现异常,但是如果我们这个项目突然宕机了呢?

例如部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,就没办法保证解锁,这个key没有被删除,所以我们需要给key设置过期时间

5.0(key过期时间)

/**
 * @author kylin
 */
@RestController
public class GoodController {

    public final String REDIS_LOCK = "REDIS_LOCK";

    @Autowired
    StringRedisTemplate stringRedisTemplate;


    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() {
        try {
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
            //redis设置过期时间
            stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
            //如果已经存在
            if (!flag) {
                return "抢锁失败!";
            }
            synchronized (this) {
                //get key 查看库存的数量够不够
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);
                if (goodsNumber > 0) {
                    //库存数量-1
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                }
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        } finally {
            //解锁
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }
}

stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);

但是这样设置key+过期时间分开了,必须要合并成一行具备原子性。

否则同样创建为key,项目宕机,同样key不会删除。我们必选要保证创建key和设置key过期时间是原子操作,必须同时成功!

6.0(key原子性)

Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

/**
 * @author kylin
 */
@RestController
public class GoodController {

    public final String REDIS_LOCK = "REDIS_LOCK";

    @Autowired
    StringRedisTemplate stringRedisTemplate;


    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() {
        try {
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
            //如果已经存在
            if (!flag) {
                return "抢锁失败!";
            }
            synchronized (this) {
                //get key 查看库存的数量够不够
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);
                if (goodsNumber > 0) {
                    //库存数量-1
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                }
            }
            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        } finally {
            //解锁
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }
}

不过还是会存在问题....

假如A线程再设置的10秒钟内没有执行完业务,key被删除后,另一个线程B就能成功设置key,再等待A线程释放锁(等待synchronized代码快外)。A线程执行业务完成后,执行删除key,但是这个key其实不是他创建的key,是B创建的key,A创建的key已经因为到期自动删除了。

7.0(超时业务 删自己的key)

所以我们要在删key操作中做判断,判断值是否相等,从而保证在过期时间内只能自己删除自己的key。

      if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
           //解锁
                stringRedisTemplate.delete(REDIS_LOCK);
      }

不过还是有原子性的问题,if判断和删除key操作不是原子性的!

如果判断成功,程序宕机,还是不能删除掉key。所以我们要保证只要进行了value值判断,相同就一定会进行删除key的操作。

8.0(删除key原子性)

使用lua脚本

/**
 * Redis工具类
 *
 * @author kylin
 */
public class RedisUtils {

    private static JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);

        jedisPool = new JedisPool(jedisPoolConfig, "192.168.56.10", 6379, 100000);
    }

    public static Jedis getJedis() throws Exception {

        if (null != jedisPool) {
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool is not ok");
    }
}

不过还是有问题~

我们要确保redisLock过期时间大于业务执行时间的问题,Redis分布式锁如何续期?

还有就是Redis集群环境下,Redis是保证AP,就会出现redis异步复制造成锁的丢失。

例如:主节点没来的及把刚刚set进来的这条数据给从节点,就挂了。。

9.0(redisson)

导入依赖

        <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
/**
 * @author kylin
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();

        //序列化配置
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }

    @Bean
    public Redisson redisson() {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379").setDatabase(0);

        return (Redisson) Redisson.create(config);
    }
}

配置注入Redisson

/**
 * @author kylin
 */
@RestController
public class GoodController {

    public final String REDIS_LOCK = "REDIS_LOCK";

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    Redisson redisson;


    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock();
        try {
            //get key 查看库存的数量够不够
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                //库存数量-1
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        } finally {
            redissonLock.unlock();
        }
    }
}

简单方便了好多。。。还强大~~

压测请求100之后,非常和谐...

不过还是可能出现以上异常,也就是当前解锁线程不是锁的持有线程

9.1

/**
 * @author kylin
 */
@RestController
public class GoodController {

    public final String REDIS_LOCK = "REDIS_LOCK";

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    Redisson redisson;


    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buyGoods")
    public String buyGoods() throws Exception {
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        RLock redissonLock = redisson.getLock(REDIS_LOCK);
        redissonLock.lock();
        try {
            //get key 查看库存的数量够不够
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            if (goodsNumber > 0) {
                //库存数量-1
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
                System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            }

            return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
        } finally {
            if (redissonLock.isLocked()){
                if (redissonLock.isHeldByCurrentThread()){
                    redissonLock.unlock();
                }
            }
        }
    }
}
  • redissonLock.isLocked()redis是否上锁
  • redissonLock.isHeldByCurrentThread()当前线程是否是锁的持有线程