redis分布式锁

601 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情

引言

应用场景:秒杀商品

分布式锁方案:本文以方案一和方案四为例子

  1. SETNX + EXPIRE
  2. SETNX + value值是(系统时间+过期时间)
  3. 使用Lua脚本(包含SETNX + EXPIRE两条指令)
  4. SET的扩展命令(SET EX PX NX)
  5. SET EX PX NX + 校验唯一随机值,再释放锁
  6. 开源框架~Redisson
  7. 多机实现的分布式锁Redlock

在这里插入图片描述

方案五可能存在「锁过期释放,业务没执行完」的问题,开源框架Redisson解决了这个问题。

给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。 在这里插入图片描述

I 并发处理

1.1 用压测模拟并发

Apache ab,模拟并发性,简单,要求低,不会占用很多的cpu,也不会占用很多内存。

ab -n 100 -c 100 接口地址(-n 表示发出100个请求 ,-c 表示100个并发)

ab -t 60-c 100 接口地址 (-t 表示连续60秒内不停发请求,-c 表示100个并发)

➜  retail git:(develop) ab
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
    -n requests     Number of requests to perform
    -c concurrency  Number of multiple requests to make at a time
    -t timelimit    Seconds to max. to spend on benchmarking
                    This implies -n 50000
    -s timeout      Seconds to max. wait for each response
                    Default is 30 seconds
    -b windowsize   Size of TCP send/receive buffer, in bytes
    -B address      Address to bind to when making outgoing connections
    -p postfile     File containing data to POST. Remember also to set -T
    -u putfile      File containing data to PUT. Remember also to set -T
    -T content-type Content-type header to use for POST/PUT data, eg.
                    'application/x-www-form-urlencoded'
                    Default is 'text/plain'

1.2 处理并发的方案

  1. synchronized处理并发: synchronized 关键字声明的方法同一时间只能被一个线程访问,synchronized 修饰符可以应用于四个访问修饰符。

(1)需要消耗大量的CPU资源

(2)synchronized 无法做到细粒度的控制,只适合单点的情况。

  1. 基于redis的分布式锁

(1)redis有很高的性能,支持分布式,高可用。 (2)更细粒度的控制,多台机器上多个进程对一个数据进行操作的互斥(比如以商品ID作为key进行控制)。

(3)redis是单线程的,对此支持的命令较好,实现起来比较方便。

II synchronized案例

2.1 synchronized案例:Java多线程安全

java的线程同步机制很大程度上都是基于内存模型而设定的。

blog.csdn.net/z929118967/…

java内存模型 :

  1. 多线程的可见性:当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。

当线程操作某个对象时,执行顺序如下:

  1. 从主存复制变量到当前工作内存 (read and load)

  2. 执行代码,改变共享变量值 (use and assign)

  3. 用工作内存数据刷新主存相关内容 (store and write)

  1. 有序性: 线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有该变量,则会从主内存中拷贝一个副本到工作内存中,这个过程为read-load,完成后线程会引用该副本。 当同一线程再度引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本 (use),也就是说 read,load,use顺序可以由JVM实现系统决定。

java用synchronized关键字做为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。

// 理论上,每个对象都可以做为锁,但一个对象做为锁时,应该被多个线程共享,这样才显得有意义,在并发环境下,一个没有共享的对象作为锁是没有意义的。
synchronized(锁){   
//     临界区代码   
}   

一个线程执行临界区代码过程如下:

  1. 获得同步锁
  2. 清空工作内存
  3. 从主存拷贝变量副本到工作内存
  4. 对这些变量计算
  5. 将变量从工作内存写回到主存
  6. 释放锁

可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

2.2 synchronized案例:OC安全隐患解决方法(互斥锁)

blog.csdn.net/z929118967/… @synchronized(锁对象) { // 需要锁定的代码 }

注意:锁定1份代码只用1把锁,用多把锁是无效的

  1. 互斥锁的优缺点 优点:能有效防止因多线程抢夺资源造成的数据安全问题 缺点:需要消耗大量的CPU资源
  2. 互斥锁的使用前提:多条线程抢夺同一块资源

线程同步的意思是:多条线程在同一条线上执行(按顺序地执行任务), 互斥锁,就是使用了线程同步技术。

/**
 atomic:原子属性,为setter方法加锁(默认就是atomic)
 nonatomic:非原子属性,不会为setter方法加锁
1. nonatomic和atomic对比
 atomic:线程安全,需要消耗大量的资源
 nonatomic:非线程安全,适合内存小的移动设备
2. iOS开发的建议
 1)所有属性都声明为nonatomic
 2)尽量避免多线程抢夺同一块资源
 3)尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。【具体情况具体分析】
 */
@property (nonatomic,strong) NSThread *thread1;
 
- (void)run{
    while (1) {
        @synchronized(self) {//加锁
            int count = self.poll;
            if (count>0) {
                [NSThread sleepForTimeInterval:0.1];
                self.poll--;
                NSLog(@"%s---name:%@,poll = %d",__func__,[[NSThread currentThread] name],self.poll);
            }else{
                break;
            }
        }//解锁
    }
}

可变的数组、字典、或者字符串不是线程安全的。

III redis分布式锁

依赖

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>


  1. SETNX key value:只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

SETNX KEY_NAME VALUE

  1. GETSET:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
GETSET key value

  1. expire key timeout:设置 key 的过期时间,key 过期后将不再可用。单位以秒计。超过这个时间锁会自动释放,避免死锁。

  2. delete key:删除key

3.1 思想

将设置值和过期时间合并成一步操作:

  1. 获取锁的时候,使用setnx加锁,并给锁加一个超时时间,超过该时间则自动释放锁。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过value判断是不是该锁,若是该锁,则执行delete进行锁释放。

获得锁的条件:锁超时或者第一次获取锁 释放锁的时机:任务完成

给锁加一个超时时间是避免异常情况没有进行解锁或者解锁异常了,造成死锁。

超时时间的控制可以使用expire命令,或者value 为当前时间+超时时间 ,自己结合当前时间进行判断。

3.2 实现

  1. 超时时间控制:使用expire命令
//加锁
  String token = UUID.randomUUID().toString();
  // NX是不存在时才set, XX是存在时才set, EX是秒,PX是毫秒
  String lock = jedis.set(key, token, "NX", "EX",TIMEOUT);

  1. 超时的实现:value 为当前时间+超时时间 ,自己结合当前时间进行判断。

从资源释放的角度,推荐redis中的key都设置expire,即便是1年的时候。

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;


@Component
@Slf4j
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     * @param key 例如:productId:123456;skuId
     * @param value 当前时间+超时时间 例如:1582891777814
     * @return
     */
    public boolean lock(String key, String value) {
        log.info("value={}",value);
        /*如果key不存在 就赋值 返回true  加锁成功  相当于reids的setnx*/
        if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
        //如果key存在 获取当前key已经存在的value
        String currentValue = redisTemplate.opsForValue().get(key);
        log.info("currentValue={}",currentValue);
        //如果锁过期
        if (!StringUtils.isEmpty(currentValue)
                && Long.parseLong(currentValue) < System.currentTimeMillis()) {/*如果当前时间大于超时时间 说明已经抢单加锁超过10秒了*/
            //获取上一个锁的时间
            String oldValue = redisTemplate.opsForValue().getAndSet(key, value);/*获取当前key的旧value 同时把新的value set进去*/
            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);
        }
    }

}


3.3 应用

模拟不同用户秒杀同一商品的请求


    private static final int TIMEOUT = 10 * 1000; //超时时间 10s

    @Autowired
    private RedisLock redisLock;//redis分布式锁



    @Override
    public void orderProductMockDiffUser(String productId)
    {
        // 加锁
        long time = System.currentTimeMillis() + TIMEOUT;
        log.info(String.valueOf(time));
        Boolean str = redisLock.lock(productId,String.valueOf(time));
        if(!str){
            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));
    }

see also

redis缓存的使用

在这里插入图片描述