您当前的位置: 首页 >  分布式

庄小焱

暂无认证

  • 3浏览

    0关注

    805博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

高并发系统设计——分布式锁解决方案

庄小焱 发布时间:2021-12-26 08:55:10 ,浏览量:3

摘要

分布式应用进行逻辑处理时经常会遇到并发问题。比如一个操作要修改用户的状态,修改状态需要先读出用户的状态, 在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题, 因为读取和保存状态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始, 就一直运行到结束,中间不会有任何 context switch 线程切换。)这个时候就要使用到分布式锁来限制程序并发的执行。本博文主要是的介绍分布式锁的原理和应用场景。

一、分布式锁实现方式

锁是一种常用的并发控制机制,用于保证一项资源在任何时候只能被一个线程使用,如果其他线程也要使用同样的资源,必须排队等待上一个线程使用完。

锁指的是程序级别的锁,例如 Java 语言中的 synchronized 和 ReentrantLock 在单应用中使用不会有任何问题, 但如果放到分布式环境下就不适用了,这个时候我们就要使用分布式锁。分布式锁比较好理解就是用于分布式环境下并发控制的一种机制, 用于控制某个资源在同一时刻只能被一个应用所使用。如下所示:

分布式锁比较常见的实现方式有三种:

  1. 基于Memcached实现的分布式锁:使用add命令,添加成功的情况下,表示创建分布式锁成功。
  2. 基于数据库分布式锁实现。
  3. 基于Redis实现的分布式锁。
  4. 基于ZooKeeper实现的分布式锁:使用ZooKeeper顺序临时节点来实现分布式锁。

二、MySQL分布式锁

三、Redis分布式锁 3.1 单机架构下的数据一致性问题

场景描述:客户端模拟购买商品过程,在Redis中设定库存总数剩100个,多个客户端同时并发购买。

package com.zhuangxiaoyan.springbootredis.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description
* 最简单的情况,没有加任何的考虑,
* 即使是单体应用,并发情况下数据一致性都有问题
* @param: null
* @date: 2022/4/9 21:25
* @return:
* @author: xjl
*/

@RestController
public class NoneController {

@Autowired
StringRedisTemplate template;

@RequestMapping("/buy")
public String index() {
// Redis中存有goods:001号商品,数量为100  相当于是的redis中的get("goods")的操作。
String result = template.opsForValue().get("goods");
// 获取到剩余商品数
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
               // 剩余商品数大于0 ,则进行扣减
int realTotal = total - 1;
// 将商品数回写数据库  相当于设置新的值的结果
template.opsForValue().set("goods", String.valueOf(realTotal));
System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
return "购买商品成功,库存还剩:" + realTotal + "件";
} else {
System.out.println("购买商品失败");
}
return "购买商品失败";
}
}

使用Jmeter模拟高并发场景,测试结果如下:

测试结果出现多个用户购买同一商品,发生了数据不一致问题!解决办法:单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性:

package com.zhuangxiaoyan.springbootredis.controller;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/**
 * @description 单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性
 * 1. synchronized
 * 2. ReentrantLock
 * 这种情况下,不会产生并发问题
 * @param: null
 * @date: 2022/4/9 21:25
 * @return:
 * @author: xjl
 */
@RestController
public class ReentrantLockController {
    // 引入的ReentrantLock 锁机制
    Lock lock = new ReentrantLock();
 
    @Autowired
    StringRedisTemplate template;
 
    @RequestMapping("/buy")
    public String index() {
        // 加锁
        lock.lock();
        try {
            // Redis中存有goods:001号商品,数量为100  相当于是的redis中的get("goods")的操作。
            String result = template.opsForValue().get("goods");
            // 获取到剩余商品数
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                int realTotal = total - 1;
                // 将商品数回写数据库  相当于设置新的值的结果
                template.opsForValue().set("goods", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
                return "购买商品成功,库存还剩:" + realTotal + "件";
            } else {
                System.out.println("购买商品失败");
            }
        } catch (Exception e) {
            //解锁
            lock.unlock();
        } finally {
            //解锁
            lock.unlock();
        }
        return "购买商品失败";
    }
}
3.2 分布式数据一致性问题

上面解决了单体应用的数据一致性问题,但如果是分布式架构部署呢,架构如下:提供两个服务,端口分别为8001、8002,连接同一个Redis服务,在服务前面有一台Nginx作为负载均衡。两台服务代码相同,只是端口不同。

将8001、8002两个服务启动,每个服务依然用ReentrantLock加锁,用Jmeter做并发测试,发现会出现数据一致性问题!

4.3 redis的set命令来实现分布式加锁
package com.zhuangxiaoyan.springbootredis.controller;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.UUID;
 
/**
 * @description 面使用redis的set命令来实现加锁
 * 1.SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
 * EX seconds − 设置指定的到期时间(以秒为单位)。
 * PX milliseconds - 设置指定的到期时间(以毫秒为单位)。
 * NX - 仅在键不存在时设置键。
 * XX - 只有在键已存在时才设置。
 * @param: null
 * @date: 2022/4/9 21:25
 * @return:
 * @author: xjl
 */
@RestController
public class RedisLockControllerV1 {
 
    public static final String REDIS_LOCK = "good_lock";
 
    @Autowired
    StringRedisTemplate template;
 
    @RequestMapping("/buy")
    public String index() {
 
        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value);
            // 加锁失败
            if (!flag) {
                return "抢锁失败!";
            }
            System.out.println(value + " 抢锁成功");
            String result = template.opsForValue().get("goods");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                int realTotal = total - 1;
                template.opsForValue().set("goods", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
                return "购买商品成功,库存还剩:" + realTotal + "件";
            } else {
                System.out.println("购买商品失败");
            }
            return "购买商品失败";
        } finally {
            // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放,所以要在finally处理 template.delete(REDIS_LOCK);
            template.delete(REDIS_LOCK);
        }
    }
}
4.4 Redis设置过期时间

如果程序在运行期间,部署了微服务jar包的机器突然挂了,代码层面根本就没有走到finally代码块,也就是说在宕机前,锁并没有被删除掉,这样的话,就没办法保证解锁,所以,这里需要对这个key加一个过期时间,Redis中设置过期时间有两种方法:

  • template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
  • template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)

第一种方法需要单独的一行代码,且并没有与加锁放在同一步操作,所以不具备原子性,也会出问题, 第二种方法在加锁的同时就进行了设置过期时间,所有没有问题,这里采用这种方式。

// 为key加一个过期时间,其余代码不变
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
package com.zhuangxiaoyan.springbootredis.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @description 在第四种情况下,如果在程序运行期间,部署了微服务的jar包的机器突然挂了,代码层面根本就没有走到finally代码块
 * 没办法保证解锁,所以这个key就没有被删除
 * 这里需要对这个key加一个过期时间,设置过期时间有两种方法
 * 1. template.expire(REDIS_LOCK,10, TimeUnit.SECONDS);第一种方法需要单独的一行代码,并没有与加锁放在同一步操作,所以不具备原子性,也会出问题
 * 2. template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);第二种方法在加锁的同时就进行了设置过期时间,所有没有问题
 * @date: 2022/4/9 21:25
 * @return:
 * @author: xjl
 */
@RestController
public class RedisLockControllerV2 {

    public static final String REDIS_LOCK = "good_lock";

    @Autowired
    StringRedisTemplate template;

    @RequestMapping("/buy")
    public String index() {

        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            // 为key加一个过期时间  10s
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
            // 加锁失败
            if (!flag) {
                return "抢锁失败!";
            }
            System.out.println(value + " 抢锁成功");
            String result = template.opsForValue().get("goods");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                int realTotal = total - 1;
                template.opsForValue().set("goods", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
                return "购买商品成功,库存还剩:" + realTotal + "件";
            } else {
                System.out.println("购买商品失败");
            }
            return "购买商品失败";
        } finally {
            // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放,所以要在finally处理  template.delete(REDIS_LOCK);
            template.delete(REDIS_LOCK);
        }
    }
}
4.5 Redis设置锁的删除

设置了key的过期时间,解决了key无法删除的问题,但问题又来了,上面设置了key的过期时间为10秒,如果业务逻辑比较复杂,需要调用其他微服务, 处理时间需要15秒(模拟场景,别较真),而当10秒钟过去之后,这个key就过期了,其他请求就又可以设置这个key, 此时如果耗时15秒的请求处理完了,回来继续执行程序,就会把别人设置的key给删除了,这是个很严重的问题!所以,谁上的锁,谁才能删除。

package com.zhuangxiaoyan.springbootredis.controller;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
 * @description
 * 在第五种情况下,设置了key的过期时间,解决了key无法删除的问题,但问题又来了
 * 我们设置了key的过期时间为10秒,如果我们的业务逻辑比较复杂,需要调用其他微服务,需要15秒
 * 10秒钟过去之后,这个key就过期了,其他请求就又可以设置这个key了
 * 但是如果耗时的请求处理完了,回来继续执行程序,就会把别人设置的key给删除了,这是个很严重的问题
 * 所以,谁上的锁,谁才能删除
 * @date: 2022/4/9 21:25
 * @return:
 * @author: xjl
 */
@RestController
public class RedislockControllerV3 {
 
    public static final String REDIS_LOCK = "good_lock";
 
    @Autowired
    StringRedisTemplate template;
 
    @RequestMapping("/buy")
    public String index() {
 
        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            // 为key加一个过期时间10s
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
            // 加锁失败
            if (!flag) {
                return "抢锁失败!";
            }
            System.out.println(value + " 抢锁成功");
            String result = template.opsForValue().get("goods");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此处需要调用其他微服务,处理时间较长。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
                return "购买商品成功,库存还剩:" + realTotal + "件";
            } else {
                System.out.println("购买商品失败");
            }
            return "购买商品失败";
        } finally {
            // 谁加的锁,谁才能删除
            if (template.opsForValue().get(REDIS_LOCK).equals(value)) {
                template.delete(REDIS_LOCK);
            }
        }
    }
}
4.6 Redis中Lua原子操作

规定了谁上的锁,谁才能删除,但finally快的判断和del删除操作不是原子操作,并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性, 最好要保证对数据的操作具有原子性。在redis中的保证原子操作的是

  1. 使用Lua脚本,进行锁的删除
  2. 使用Redis事务来实现原子操作
package com.zhuangxiaoyan.springbootredis.controller;
 
import com.zhuangxiaoyan.springbootredis.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
 
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
 
/**
 * @description 在第六种情况下,规定了谁上的锁,谁才能删除
 * 但finally快的判断和del删除操作不是原子操作,并发的时候也会出问题
 * 并发就是要保证数据的一致性,保证数据的一致性,最好要保证对数据的操作具有原子性
 * @param: null
 * @date: 2022/4/9 21:25
 * @return:
 * @author: xjl
 */
 
@RestController
public class RedisLockControllerV4 {
 
    public static final String REDIS_LOCK = "good_lock";
 
    @Autowired
    StringRedisTemplate template;
 
    /**
     * @description 使用Lua脚本,进行锁的删除
     * @param:
     * @date: 2022/4/9 21:56
     * @return: java.lang.String
     * @author: xjl
     */
    @RequestMapping("/buy")
    public String index() {
 
        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            // 为key加一个过期时间
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
            // 加锁失败
            if (!flag) {
                return "抢锁失败!";
            }
            System.out.println(value + " 抢锁成功");
            String result = template.opsForValue().get("goods");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此处需要调用其他微服务,处理时间较长。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
                return "购买商品成功,库存还剩:" + realTotal + "件";
            } else {
                System.out.println("购买商品失败");
            }
            return "购买商品失败,服务端口为8001";
        } finally {
            // 谁加的锁,谁才能删除  使用Lua脚本,进行锁的删除
            Jedis jedis = null;
            try {
                jedis = RedisUtils.getJedis();
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then " +
                        "return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
                Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if ("1".equals(eval.toString())) {
                    System.out.println("-----del redis lock ok....");
                } else {
                    System.out.println("-----del redis lock error ....");
                }
            } catch (Exception e) {
                System.out.println(e.getMessage());
            } finally {
                if (null != jedis) {
                    jedis.close();
                }
            }
        }
    }
 
    /**
     * @description 使用redis事务
      * @param:
     * @date: 2022/4/9 21:56
     * @return: java.lang.String
     * @author: xjl
    */
    @RequestMapping("/buy2")
    public String index2() {
 
        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            // 为key加一个过期时间
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
            // 加锁失败
            if (!flag) {
                return "抢锁失败!";
            }
            System.out.println(value + " 抢锁成功");
            String result = template.opsForValue().get("goods");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此处需要调用其他微服务,处理时间较长。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
                return "购买商品成功,库存还剩:" + realTotal + "件";
            } else {
                System.out.println("购买商品失败");
            }
            return "购买商品失败,服务端口为8001";
        } finally {
            // 谁加的锁,谁才能删除 ,使用redis事务
            while (true) {
                template.watch(REDIS_LOCK);
                if (template.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
                    template.setEnableTransactionSupport(true);
                    template.multi();
                    template.delete(REDIS_LOCK);
                    List list = template.exec();
                    if (list == null) {
                        continue;
                    }
                }
                template.unwatch();
                break;
            }
        }
    }
}
4.7 Redis集群下的分布式锁

规定了谁上的锁,谁才能删除,并且解决了删除操作没有原子性问题。但还没有考虑缓存续命,以及Redis集群部署下,异步复制造成的锁丢失: 主节点没来得及把刚刚set进来这条数据给从节点,就挂了。所以直接上RedLock的Redisson落地实现。

package com.zhuangxiaoyan.springbootredis.controller;
 
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.UUID;
/**
 * @description
 * 在第六种情况下,规定了谁上的锁,谁才能删除
 * 1. 缓存续命
 * 2. redis异步复制造成的锁丢失:主节点没来得及把刚刚set进来这条数据给从节点,就挂了
 * @param: null
 * @date: 2022/4/9 21:25
 * @return:
 * @author: xjl
 */
 
@RestController
public class RedisLockControllerV5 {
 
    public static final String REDIS_LOCK = "good_lock";
 
    @Autowired
    StringRedisTemplate template;
 
    @Autowired
    Redisson redisson;
 
    @RequestMapping("/buy")
    public String index() {
        RLock lock = redisson.getLock(REDIS_LOCK);
        lock.lock();
        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-", "");
        try {
            String result = template.opsForValue().get("goods");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此处需要调用其他微服务,处理时间较长。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
                return "购买商品成功,库存还剩:" + realTotal + "件";
            } else {
                System.out.println("购买商品失败");
            }
            return "购买商品失败";
        } finally {
            // 如果锁依旧在同时还是在被当前线程持有,那就解锁。 如果是其他的线程持有 那就不能释放锁资源
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
四、Zookeeper分布式锁

五、分布式锁总结

参考博文

系统设计解决方案/1-分布式锁方案/分布式锁解决方案.md · 庄小焱/SeniorArchitect - Gitee.com

如何用Redis实现分布式锁? - 掘金

面试官:怎么用Zk(Zookeeper)实现实现分布式锁呀? - 掘金

关注
打赏
1657692713
查看更多评论
立即登录/注册

微信扫码登录

0.0431s