分布式锁浅谈之redis锁

Redis 分布式锁

业务:用于库存扣减,因为是微服务架构所以 jvm 级别的锁都不满足需求。故考虑分布式锁

A:服务获取锁 修改库存 释放锁

背景:

在单机时代,虽然不存在分布式锁,但也会面临资源互斥的情况,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就需要对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。

但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。因此,为了解决这个问题,「分布式锁」就强势登场了。

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

分布式锁的特性是

  • 排他性(在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取)、
  • 避免死锁(把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放))、
  • 高可用(获取或释放锁的机制必须高可用且性能佳)。

目前相对主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:

  • 数据库(MySQL)
  • Redis
  • ZooKeeper

分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)、Redis的setnx()命令、Zookeeper(在某个持久节点添加临时有序节点,判断当前节点是否是序列中最小的节点,如果不是则监听比当前节点还要小的节点。如果是,获取锁成功。当被监听的节点释放了锁(也就是被删除),会通知当前节点。然后当前节点再尝试获取锁,如此反复)

1.数据库锁,mysql 行锁,悲观锁。 (for update)

通过数据库事务+行锁解决库存扣减问题。

缺点:危险性较高,需要手动提交事物,所以死锁危险性较高。如果忘记提交事物,则会死锁。不过可以通过wait 参数设置等待时间。死锁后可化解。

事务中可能会有其他逻辑,导致数据库锁时间较长,影响性能

注意:查询需要有明确主键,如果没有主键则为表锁,非行锁 所有涉及库存操作统一使用for update wait 枷锁,for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效,要关闭autocommit 配置。所以库存操作要手动提交事物 “begin,start tarn”

示例

悲观锁

begin;

select * from goods where id = 1 for update;

update goods set stock = stock - 1 where id = 1;

commit;

乐观锁:设计version字段, 允许读取不准确数值

\#不加锁获取 id=1 的商品对象

select  from goods where id = 1

begin;

#更新 stock 值,这里需要注意 where 条件 “stock = cur_stock”,只有程序中获取到的库存量与数据库中的库存量相等才执行更新

update goods set stock = stock - 1 where id = 1 and stock = cur_stock;

commit;

2. 单机 redis 分布式锁

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

JedisConnectionFactory和LettuceConnectionFactory 中的 setIfAbsent(key, value) 方法 :如果 key 存在则返回 false 如果key 不存在则插入 ,且返回true ;

方法

1、直接setnx,直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴

缺点:如果setnx成功,还没来得及释放,服务挂了,那么这个key永远都不会被获取到

用法:(java 语法:redisTemplate.opsForValue().setIfAbsent(key, value) )

注意: 在事物或者pipline 中 setIfAbsent 方法的返回值为 null

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> 
  • 2、setnx设置一个过期时间
    为了改正第一个方法的缺陷,我们用setnx获取锁,然后用expire对其设置一个过期时间,如果服务挂了,过期时间一到自动释放
    缺点:setnx和expire是两个方法,不能保证原子性,如果在setnx之后,还没来得及expire,服务挂了,还是会出现锁不释放的问题

  • 3、set nx px
    redis官方为了解决第二种方式存在的缺点,在Redis 2.6.12本本 为set指令添加了扩展参数nx和ex,保证了setnx+expire的原子性,使用方法:

    // java 使用方法
    //此方法需要使用 JedisConnectionFactory 链接方式 
    redisTemplate.opsForValue().setIfAbsent(key, value, 200, TimeUnit.MILLISECONDS)
    // redis 语法
    set key value ex 5 nx
    
    

    缺点
    ①如果在过期时间内,事务还没有执行完,锁提前被自动释放,其他的线程还是可以拿到锁
    ②上面所说的那个缺点还会导致当前的线程释放其他线程占有的锁

    上面所说的第一个缺点,没有特别好的解决方法,只能把过期时间尽量设置的长一点,并且最好不要执行耗时任务
    第二个缺点,可以理解为当前线程有可能会释放其他线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁,即setnx的时候将value设为任务的唯一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,其实也是变相地解决了第一个问题
    缺点:get key和将value与id比较是两个步骤,不能保证原子性

        String lockValue = redisTemplate.opsForValue().get(lock);
        if (StringUtils.isNoneBlank(lockValue) && lockValue.equals(value)) {
            //解锁
            redisTemplate.opsForValue().getOperations().delete(lock);
        }

完整配置使用java代码:

配置redisConfig

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(RedisConfig.class);
    @Value("${spring.redis.database}")
    private Integer database;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private Integer port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.lettuce.pool.max-active}")
    private Integer maxActive;
    @Value("${spring.redis.lettuce.pool.max-wait}")
    private Integer maxWait;
    @Value("${spring.redis.lettuce.pool.max-idle}")
    private Integer maxIdle;
    @Value("${spring.redis.lettuce.pool.min-idle}")
    private Integer minIdle;
    @Value("${spring.redis.lettuce.shutdown-timeout}")
    private Integer timeout;

    @Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate() {
        return getTemplate(redisConnectionFactory());
    }

    private RedisConnectionFactory redisConnectionFactory() {
        return connectionFactory(maxActive, maxIdle, minIdle, maxWait, host, password, timeout, port, database);
    }


    /**
     * 创建连接
     *
     * @param maxActive
     * @param maxIdle
     * @param minIdle
     * @param maxWait
     * @param host
     * @param password
     * @param timeout
     * @param port
     * @param database
     * @return
     */
    private JedisConnectionFactory connectionFactory(Integer maxActive,
                                                     Integer maxIdle,
                                                     Integer minIdle,
                                                     Integer maxWait,
                                                     String host,
                                                     String password,
                                                     Integer timeout,
                                                     Integer port,
                                                     Integer database) {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setDatabase(database);
        redisStandaloneConfiguration.setPassword(RedisPassword.of(password));

        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxActive);
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWait);

        JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jpcb =
                (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder();
        //指定jedisPoolConifig来修改默认的连接池构造器(真麻烦,滥用设计模式!)
        jpcb.poolConfig(jedisPoolConfig);
        //通过构造器来构造jedis客户端配置
        JedisClientConfiguration jedisClientConfiguration = jpcb.build();
        //单机配置 + 客户端配置 = jedis连接工厂
        return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration);
    }

    /**
     * 创建 RedisTemplate 连接类型,此处为hash
     *
     * @param factory
     * @return
     */
    private RedisTemplate<Object, Object> getTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setValueSerializer(jackson2JsonRedisSerializer(new ObjectMapper()));
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer(new ObjectMapper()));

        template.afterPropertiesSet();
        return template;
    }

    /**
     * 对value 进行序列化
     *
     * @param objectMapper
     * @return
     */
    private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(ObjectMapper objectMapper) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        return jackson2JsonRedisSerializer;
    }
}

使用:

 /**
     * 加锁
     * 当然在获取锁的时候 可以判断如果获取失败 增加重试机制
     * @param key
     * @return
     */
    private String lock(String key) throws ServiceException {
        String value = String.valueOf(System.currentTimeMillis()) + new Random().nextInt(1000);
        Boolean result = redisTemplate.opsForValue().setIfAbsent(skuCode, value, 200, TimeUnit.MILLISECONDS);
        if(result){
        	return value;
        }
        return null;
    }
    

    
   /**
     * 释放锁
     *
     * @param lock
     */
    private void unLock(String lock, String value) throws ServiceException {
        String lockValue = redisTemplate.opsForValue().get(lock);
        if (StringUtils.isNoneBlank(lockValue) && lockValue.equals(value)) {
            //解锁
            redisTemplate.opsForValue().getOperations().delete(lock);
        }
    }

集群Redis 分布式锁

注意:Java环境有 Redisson 可用于生产环境集群Redis 分布式锁

在Redis的分布式环境中,Redis 的作者提供了RedLock 的算法来实现一个分布式锁。RedLock算法加锁步骤如下

加锁

  1. 获取当前Unix时间,以毫秒为单位。
  2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

解锁

向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.

Zookeeper 分布式锁

分布式锁还是Zookeeper会比较好,只是比较麻烦一些。

参考文档:

https://blog.csdn.net/qq_35349114/article/details/84027745

https://www.cnblogs.com/mengchunchen/p/9647756.html

https://www.jianshu.com/p/f8a8e49362dc

https://juejin.im/post/5b737b9b518825613d3894f4

发布了343 篇原创文章 · 获赞 649 · 访问量 231万+

猜你喜欢

转载自blog.csdn.net/u012373815/article/details/101623159