04.分布式锁及其原理

一、管道 与 Lua 脚本

管道(Pipeline)

客户端一次性发送多个请求,不用等待服务器响应,待所有脚本执行完成后一起响应。客户端用 pipeline 方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。pipeline 中发送的每个命令都会被 server 立即执行,如果执行失败,将会在此后的响应中得到信息,不会影响管道里面后续其他命令的执行。

注意: 管道不是原子的,但 Redis 提供的批量操作命令(例如 MSET)是原子的

Lua脚本

Redis 2.6 版本开始推出,可以使用 Lua 语言编写脚本传到 Redis 中执行。使用 Lua脚本有如下优点:

  • 减少内存开销: 多次网络请求(Redis 逻辑)可以通过一个请求完成,可以减少网络往返时延。
  • 原子操作: Redis 会将整个 Lua 脚本作为一个原子操作执行,这个期间其他命令无法插入。
  • 替代redis的事务功能: Redis 自带事务出错不支持回滚,十分鸡肋,Lua 脚本支持常规事务功能,Redis 官方也推荐使用 Lua 脚本代替 Redis 的事务
#分布式锁使用的一个Lua脚本,不存在即删除
#在 Lua 脚本中,可以使用redis.call()函数来执行Redis命令
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
// SpringBoot 整合RedisTemplate执行Lua脚本
// script是上面的Lua脚本
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

注意:Redis 是单线程执行脚本的,所以不要在 Redis 中出现耗时的运算或死循环,否则会阻塞 Redis。

还可以使用 EVAL 命令对 Lua 脚本进行求值,EVAL命令的格式如下:

# script是 Lua 脚本
# numkeys 参数用于指定键名参数的个数
# key [key ...] 键名参数,表示在脚本中所用到的哪些Redis键(key)
# arg [arg ...] 不是键名的附加参数,
EVAL script numkeys key [key ...] arg [arg ...]

#使用示例
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
#输出
1) "key1"
2) "key2"
3) "first"
4) "second"

下面是购买商品减库存示例:

// KEYS[] 对应第一个 List 集合中的数据,ARGV[] 对应第二个集合中的数据
jedis.set("product_stock_10016", "15");  //初始化商品10016的库存
String script = " local count = redis.call('get', KEYS[1]) " +
                " local a = tonumber(count) " +
                " local b = tonumber(ARGV[1]) " +
                " if a >= b then " +
                "   redis.call('set', KEYS[1], a-b) " +
                "   return 1 " +
                " end " +
                " return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);

Redis 实现分布式锁

在分布式的情况下,Synchronize 等同步机制无法对不同服务器之间的对象进行加锁,为了解决这一问题,出现了分布式锁。

1.下列代码在分布式系统下失效

@Autowired
StringRedisTemplate redisTemplate;

@PostMapping(/reduce_stock)
public void reduceStock() {
    synchronized(this) {
        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            int nowStock = stock - 1;
            redisTemplate.opsForValue().set("stock",nowStock + "");
            System.out.println("减库存成功,剩余库存:" + nowStock + "");
        } else {
            System.out.println("库存不足:" + nowStock + "");
        }
    }   
}

2.上述代码锁分布式情况下失效,引入分布式锁

@Autowired
StringRedisTemplate redisTemplate;

@PostMapping(/reduce_stock)
public void reduceStock() {
    String lockKey = "product_lock_01";
    try {
        // 占分布式锁,这两条命令非原子操作,使用下面的合体
        // Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"")
        // redisTemplate.expire(lcokKey,30,TimeUnit.SECONDS);
        
        // 占分布式锁,设置30s超时
        String uuid = UUID.randomUUID().toString();
        Boolean isLock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        
        if(isLock) {
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if(stock > 0){
                int nowStock = stock - 1;
                redisTemplate.opsForValue().set("stock",nowStock + "");
                System.out.println("减库存成功,剩余库存:" + nowStock + "");
            } else {
                System.out.println("库存不足:" + nowStock + "");
            }
        } else {
            System.out.println("获取分布式锁失败...等待重试...");
            //加锁失败...自旋重试机制
            //休眠一百毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            reduceStock();
        }
    } finally {
        // 删除锁
        redisTemplate.delete(lockKey);
    }   
}

3.上述代码还是存在问题,锁失效,在低并发的情况下运行存在,假如我们第一个线程由于某些业务逻辑较大需要执行 35 秒,但是我们的自动过期时间是 30 秒,此时第一个线程锁失效后,第二个线程进来,发现可以设置锁成功,此时第一个线程执行完成最后的 finally 逻辑,删除的是第二个线程设置的锁 ,一直循环,分布式锁失效,造成时灵时不灵的错误。

@Autowired
StringRedisTemplate redisTemplate;

@PostMapping(/reduce_stock)
public void reduceStock() {
    String lockKey = "product_lock_01";
    String uuid = UUID.randomUUID().toString();
    try {
        // 占分布式锁,这两条命令非原子操作,使用下面的合体
        // Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"")
        // redisTemplate.expire(lcokKey,30,TimeUnit.SECONDS);
        
        // 占分布式锁,设置30s超时
        Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid,300,TimeUnit.SECONDS);
        
        if(isLock) {
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if(stock > 0){
                int nowStock = stock - 1;
                redisTemplate.opsForValue().set("stock",nowStock + "");
                System.out.println("减库存成功,剩余库存:" + nowStock + "");
            } else {
                System.out.println("库存不足:" + nowStock + "");
            }
        } else {
            System.out.println("获取分布式锁失败...等待重试...");
            //加锁失败...自旋重试机制
            //休眠一百毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            reduceStock();
        }
    } finally {
        // 非原子操作,推荐使用Lua脚本
//			if(uuid.equals(redisTemplate.opsForValue().get(lockKey))){
//				 redisTemplate.delete(lockKey);
//    		}
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        //删除锁
        redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(lockKey), uuid);
    }
}

4.上述代码还是存在一些问题,但是一般互联网情况下代码实现到这种程度已经差不多了(一般设置的锁失效时间绝对大于业务代码执行时间,如果出现问题,大多是业务代码有问题)。但也存在一些极端的情况,因此可以使用锁续存,在锁过来多久未释放进行一次续存操作。此功能在 Redisson 中有实现。

Redisson

Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中包括(BitSet、 Set、Multimap、 SortedSet、Map、BlockingQueue、List、Queue、,Deque、BlockingDeque、,Semaphore、Lock、AtomicLong、CountDownLatch、Publish / Subscribe、Bloom filter、Remote service、Spring cache、Executor service、Live Object service、Scheduler service)。

Redisson 的使用

第一步:导入依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.11.1</version>
</dependency>

第二步:配置Redisson

配置Redisson 官方文档 https://github.com/redisson/redisson/wiki

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的操作都是通过该对象
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    RedissonClient redisson() throws IOException {
        // 1.创建配置
        Config config = new Config();
        // 单节点模式 根据Config创建出RedissonClient
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");

        // 集群模式
//        config.useClusterServers()
//                .addNodeAddress("redis://127.0.0.1:7004", "redis://127.0.0.1:7001");
        return Redisson.create(config);
    }
}

前面减库存代码优化,使用 Redisson 加解锁,并续存

@Autowired
StringRedisTemplate redisTemplate;

@Autowired
Redisson redisson;

@PostMapping(/reduce_stock)
public void reduceStock() {
    
    String lockKey = "product_lock_01";
    RLock resissonLock = redisson.getLock(lockKey);  
    try {
        // 占分布式锁,设置30s超时
        Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid,300,TimeUnit.SECONDS);
        
        // 加锁,底层实现了锁续存
        // 获取一把锁,只要锁名字一样就是同一把锁,他会自己去Reddish中创建一个key为my-lock的数据
        // 底层原越是使用sexnx和Lua脚本
        // 阻塞式等待(自旋),默认加锁30s
        // (1) 锁的自动续期,如果业务时间超长,运行期间自动给锁续上新的30s,不用担心业务时间超长,锁自动被删除
        // (2) 加锁业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动过期,看门狗机制
        // 可以通过修改Config.lockWatchdogTimeout来另行指定过期时间
        // lock.lock(10, TimeUnit.SECONDS); 是要自己指定超时时间不会自动续期,一定要大于业务时间
        // 如果传递了超时时间,就会发送给Redis执行lua脚本,并设置传递的超时时间
        // 如果没有传递超时时间,自动使用30s,如果占锁成功,就会启动一个定时任务(重新给Redis设置过期时间 续期1/3超时时间)
        // 30 -> 20 -> 30 ->20 每隔十秒,自动续期10秒

        // 最佳方案
        // (1) lock.lock(30, TimeUnit.SECONDS); 可以省去续期,业务如果超时,一般都是有问题的
        redissonLock.lock();
        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            int nowStock = stock - 1;
            redisTemplate.opsForValue().set("stock",nowStock + "");
            System.out.println("减库存成功,剩余库存:" + nowStock + "");
        } else {
            System.out.println("库存不足:" + nowStock + "");
        }
        }
    } finally {
        resissonLock.unLock();
    }
}
Redisson 的其他工具类

读写锁

/**
 * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
 * 写锁没释放读锁必须等待
 * 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
 * 写 + 读 :必须等待写锁释放
 * 写 + 写 :阻塞方式
 * 读 + 写 :有读锁。写也需要等待
 * 写锁没释放,都必须等待
 * @return
 */
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
    String s = "";
    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
    RLock rLock = readWriteLock.writeLock();
    try {
        //1、改数据加写锁,读数据加读锁
        rLock.lock();
        s = UUID.randomUUID().toString();
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        ops.set("writeValue",s);
        TimeUnit.SECONDS.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }

    return s;
}

@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
    String s = "";
    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
    //加读锁
    RLock rLock = readWriteLock.readLock();
    try {
        rLock.lock();
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        s = ops.get("writeValue");
        try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }

    return s;
}

Semaphore

/**
 * 车库停车
 * 3车位
 * 信号量也可以做分布式限流,参照 Semaphore
 */
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {

    RSemaphore park = redisson.getSemaphore("park");
    //        park.acquire();     //获取一个信号、获取一个值,占一个车位
    boolean flag = park.tryAcquire(); // 尝试获取,不行就算了

    if (flag) {
        //执行业务
    } else {
        return "error";
    }

    return "ok=>" + flag;
}

/**
 * 释放车位
 * @return
 */
@GetMapping(value = "/go")
@ResponseBody
public String go() {
    RSemaphore park = redisson.getSemaphore("park");
    park.release();     //释放一个车位
    return "ok";
}

CountDownLanch

/**
 * 放假、锁门
 * 1班没人了
 * 5个班,全部走完,我们才可以锁大门
 * 分布式闭锁
 */

@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {

    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.trySetCount(5);
    door.await();       //等待闭锁完成

    return "放假了...";
}

@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.countDown();       //计数-1

    return id + "班的人都走了...";
}

缓存里的数据如何和数据库的数据保持一致?

1)、双写模式 改完数据库同时修改缓存中数据  解决:①写操作加锁 ②如果允许暂时性不一致,加过期时间(最终一致性)
2)、失效模式 修改后删除缓存
读写锁,读多写少情况,写加分布式锁

怎么提高分布式锁的效率

参考 ConcurrentHashMap,使用分段锁实现,比如一个需要秒杀商品的库存为10000个,我们可以将它分成 5 段,每段 2000 个库存,每次请求时去某一段减库存,如果为如果某一段为 0 时,去其他段找

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: Redis分布式锁适用于多个应用实例之间需要协调互斥访问同一个资源的场景。它可以帮助我们控制分布式系统对共享资源的访问,避免并发问题和重复操作。常见的使用场景包括: 1. 分布式任务调度:多个应用实例需要定时执行某个任务,通过使用分布式锁可以确保只有一个实例执行任务,避免重复执行。 2. 分布式缓存更新:多个应用实例需要同时更新某个缓存数据,通过使用分布式锁可以保证只有一个实例进行更新,避免数据不一致。 3. 分布式资源竞争:多个应用实例需要竞争某个资源,例如分布式锁可以用于实现分布式限流、分布式排他性操作等场景。 需要注意的是,分布式锁的实现需要考虑到高并发、死锁、误删等问题,需要根据具体的应用场景进行优化和测试。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *3* [Redis 分布式锁的实现原理和应用场景](https://blog.csdn.net/weixin_43025343/article/details/131081958)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Redis实现分布式锁及其应用场景](https://blog.csdn.net/Crime11/article/details/130132324)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值