从单体锁到分布式锁

从单体锁到分布式锁

单体锁

1. synchronized

  • 同步代码块
Object obj = new Object();
synchronized(obj){
    //需要被同步的代码块
}

synchronized(this){}
上面两种锁的都是对象

synchronize(TheClass.class){

}
这种锁住的是一个类
  • 同步方法
public synchronized void testThread()
{
    //需要被同步的代码块
}

2. 同步锁 ReentrantLock

JDK 1.7之后引入的JUC包中的重要工具类,让线程同步变得如此丝滑

class A
{
    private final ReentrantLock lock=new ReentrantLock();
    public void method()
    {
        lock.lock();
        try{
                //需要被同步的代码块
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                lock.unlock();
        }
    }
}

这是一种功能更为强大的线程同步机制,通过显式定义同步锁对象来实现同步,这里的同步锁由Lock对象充当。使用Lock与使用同步代码块有点类似,只是使用Lock时可以显示使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。

其中,为了确保能够在必要的时候释放锁,代码中使用finally来确保锁的释放,来防止死锁!

分布式锁

上面两种都是单体锁,跨JVM的情况下不好使,如果需要解决分布式服务下的资源竞争问题,需要引入分布式锁。

3. 基于数据库排他锁实现分布式锁

基于排他锁(for update)实现

实现方式
  • 获取锁可以通过,在select语句后增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁,我们可以认为获得排它锁的线程即可获得分布式锁;
  • 其余实现与使用唯一索引相同;
  • 释放锁通过connection.commit();操作,提交事务来实现。

for update具体可参考数据库-MySQL中for update的作用和用法一文。

select  … for update

select @@autocommit;


给一行数据加锁 需要把自动提交关闭
set @@autocommit = 0;
在执行 select * from xxx where 。。。for update之后
另外一个会话是没办法通过 select * from xxx where 。。。for update 查询出数据的
但是可以通过select * from xxx where 。。。插出数据,
也就是说 可以读数据,不能写数据。

优点:

简单方便,易于理解,易于操作

缺点:

  • 排他锁会占用连接,产生连接爆满的问题;
  • 如果表不大,可能并不会使用行锁;
  • 同样存在单点问题、并发量问题。

实现

伪代码
CREATE TABLE `methodLock` (
    `id` INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `method_name` VARCHAR ( 64 ) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
    `desc` VARCHAR ( 1024 ) NOT NULL DEFAULT '备注信息',
    `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
    PRIMARY KEY ( `id` ),
    UNIQUE KEY `uidx_method_name` ( `method_name ` ) USING BTREE 
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '锁定中的方法';

/**
 * 加锁
 */
public boolean lock() {
    // 开启事务
    connection.setAutoCommit(false);
    // 循环阻塞,等待获取锁
    while (true) {
        // 执行获取锁的sql
        result = select * from methodLock where method_name = xxx for update;
        // 结果非空,加锁成功
        if (result != null) {
            return true;
        }
    }
    // 加锁失败
    return false;
}

/**
 * 解锁
 */
public void unlock() {
    // 提交事务,解锁
    connection.commit();
}

SpringBoot +JPA案例

https://www.cnblogs.com/dalaoyang/p/11214159.html

4. 基于Redis的Setnx实现分布式锁

加锁
set resource_name my_random_value NX PX 30000
释放锁
delete
释放锁需要lua脚本
if redis.call("get",KEYS[1])==ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 0
end	



4.1、加锁

加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。

SET lock_key random_value NX PX 5000

值得注意的是:
random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。

这样,如果上面的命令执行成功,则证明客户端获取到了锁。

4.2、解锁

解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。

为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0 
end
4.3、在SpringBoot中的具体实现

首先,我们在pom文件中,引入Jedis。在这里,笔者用的是最新版本,注意由于版本的不同,API可能有所差异。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.1</version>
</dependency>

加锁的过程很简单,就是通过SET指令来设置值,成功则返回;否则就循环等待,在timeout时间内仍未获取到锁,则获取失败。

@Service
public class RedisLock {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    private String lock_key = "redis_lock"; //锁键

    protected long internalLockLeaseTime = 30000;//锁过期时间

    private long timeout = 999999; //获取锁的超时时间

    
    //SET命令的参数 
    SetParams params = SetParams.setParams().nx().px(internalLockLeaseTime);

    @Autowired
    JedisPool jedisPool;

    
    /**
     * 加锁
     * @param id
     * @return
     */
    public boolean lock(String id){
        Jedis jedis = jedisPool.getResource();
        Long start = System.currentTimeMillis();
        try{
            for(;;){
                //SET命令返回OK ,则证明获取锁成功
                String lock = jedis.set(lock_key, id, params);
                if("OK".equals(lock)){
                    return true;
                }
                //否则循环等待,在timeout时间内仍未获取到锁,则获取失败
                long l = System.currentTimeMillis() - start;
                if (l>=timeout) {
                    return false;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            jedis.close();
        }
    }
}

解锁我们通过jedis.eval来执行一段LUA就可以。将锁的Key键和生成的字符串当做参数传进来。

    /**
     * 解锁
     * @param id
     * @return
     */
    public boolean unlock(String id){
        Jedis jedis = jedisPool.getResource();
        String script =
                "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                        "   return redis.call('del',KEYS[1]) " +
                        "else" +
                        "   return 0 " +
                        "end";
        try {
            Object result = jedis.eval(script, Collections.singletonList(lock_key), 
                                    Collections.singletonList(id));
            if("1".equals(result.toString())){
                return true;
            }
            return false;
        }finally {
            jedis.close();
        }
    }

最后,我们可以在多线程环境下测试一下。我们开启1000个线程,对count进行累加。调用的时候,关键是唯一字符串的生成。这里,笔者使用的是Snowflake算法。

@Controller
public class IndexController {

    @Autowired
    RedisLock redisLock;
    
    int count = 0;
    
    @RequestMapping("/index")
    @ResponseBody
    public String index() throws InterruptedException {

        int clientcount =1000;
        CountDownLatch countDownLatch = new CountDownLatch(clientcount);

        ExecutorService executorService = Executors.newFixedThreadPool(clientcount);
        long start = System.currentTimeMillis();
        for (int i = 0;i<clientcount;i++){
            executorService.execute(() -> {
            
                //通过Snowflake算法获取唯一的ID字符串
                String id = IdUtil.getId();
                try {
                    redisLock.lock(id);
                    count++;
                }finally {
                    redisLock.unlock(id);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        long end = System.currentTimeMillis();
        logger.info("执行线程数:{},总耗时:{},count数为:{}",clientcount,end-start,count);
        return "Hello";
    }
}

优点: 简单

缺点:不具备可重入性。

5. 基于Zookeeper的原生语法实现分布式锁

原理

大概就是借助zk的瞬时节点有序性Watcher

https://juejin.cn/post/6844903729406148622#comment

6. 基于Zookeeper工具类curator实现分布式锁【推荐】

zookeeper的四种节点类型

1、持久化节点 :所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。

2、****持久化顺序节点****:这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。****基于持久顺序节点原理的经典应用-分布式唯一ID生成器****。

3、临时节点:和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点,集群zk环境下,****同一个路径的临时节点只能成功创建一个,利用这个特性可以用来实现master-slave选举****。

4、****临时顺序节点****:相对于临时节点而言,临时顺序节点比临时节点多了个有序,也就是说每创建一个节点都会加上节点对应的序号,先创建成功,序号越小。其经典应用场景为实现分布式锁

监视器(watcher)

当zookeeper创建一个节点时,会注册一个该节点的监视器,当节点状态发生改变时,watch会被触发,zooKeeper将会向客户端发送一条通知(就一条,因为watch只能被触发一次)。

原理

Curator内部是通过InterProcessMutex(可重入锁)来在zookeeper中创建临时有序节点实现的,之前说过,如果通过临时节点及watch机制实现锁的话,这种方式存在一个比较大的问题:所有取锁失败的进程都在等待、监听创建的节点释放,很容易发生"羊群效应",zookeeper的压力是比较大的,而临时有序节点就很好的避免了这个问题,Curator内部就是创建的临时有序节点。

基本原理:

创建临时有序节点,每个线程均能创建节点成功,但是其序号不同,只有序号最小的可以拥有锁,其它线程只需要监听比自己序号小的节点状态即可

基本思路如下:

1、在你指定的节点下创建一个锁目录lock;

2、线程X进来获取锁在lock目录下,并创建临时有序节点;

3、线程X获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁;

4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应);

5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。

代码实现

package cn.stylefeng.guns.modular.es.entity;

/**
 * @author: allen
 * @date: 2020/11/24 23:11
 * @version: 1.0
 */
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
 * classname:DistributedLock
 * desc:基于zookeeper的开源客户端Cruator实现分布式锁
 * author:simonsfan
 */
public class DistributedLock {
    public static Logger log = LoggerFactory.getLogger(DistributedLock.class);
    private InterProcessMutex interProcessMutex;  //可重入排它锁
    private String lockName;  //竞争资源标志
    private String root = "/distributed/lock/";//根节点
    private static CuratorFramework curatorFramework;
    private static String ZK_URL = "zookeeper1.tq.master.cn:2181,zookeeper3.tq.master.cn:2181,zookeeper2.tq.master.cn:2181,zookeeper4.tq.master.cn:2181,zookeeper5.tq.master.cn:2181";
    static{
        curatorFramework= CuratorFrameworkFactory.newClient(ZK_URL,new ExponentialBackoffRetry(1000,3));
        curatorFramework.start();
    }
    /**
     * 实例化
     * @param lockName
     */
    public DistributedLock(String lockName){
        try {
            this.lockName = lockName;
            interProcessMutex = new InterProcessMutex(curatorFramework, root + lockName);
        }catch (Exception e){
            log.error("initial InterProcessMutex exception="+e);
        }
    }
    /**
     * 获取锁
     */
    public void acquireLock(){
        int flag = 0;
        try {
            //重试2次,每次最大等待2s,也就是最大等待4s
            while (!interProcessMutex.acquire(2, TimeUnit.SECONDS)){
                flag++;
                if(flag>1){  //重试两次
                    break;
                }
            }
        } catch (Exception e) {
            log.error("distributed lock acquire exception="+e);
        }
        if(flag>1){
            log.info("Thread:"+Thread.currentThread().getId()+" acquire distributed lock  busy");
        }else{
            log.info("Thread:"+Thread.currentThread().getId()+" acquire distributed lock  success");
        }
    }
    /**
     * 释放锁
     */
    public void releaseLock(){
        try {
            if(interProcessMutex != null && interProcessMutex.isAcquiredInThisProcess()){
                interProcessMutex.release();
                curatorFramework.delete().inBackground().forPath(root+lockName);
                log.info("Thread:"+Thread.currentThread().getId()+" release distributed lock  success");
            }
        }catch (Exception e){
            log.info("Thread:"+Thread.currentThread().getId()+" release distributed lock  exception="+e);
        }
    }
}

业务层使用时要记得释放锁。要特别注意的是 interProcessMutex.acquire(2, TimeUnit.SECONDS)方法,可以设定等待时候,加上重试的次数,即排队等待时间= acquire × 次数,这两个值一定要设置好,因为使用了分布式锁之后,接口的TPS就下降了,没获取到锁的接口都在等待/重试,如果这里设置的最大等待时间4s,这时并发进来1000个请求,4秒内处理不完1000个请求怎么办呢?所以一定要设置好这个重试次数及单次等待时间,根据自己的压测接口设置合理的阈值,避免业务流转发生问题!

7. 基于Redission分布式锁【推荐】

文档: https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

使用javaAPI

RLock lock = redisson.getLock("myLock");

// traditional lock method
lock.lock();

// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);

// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

使用SpringBootStarter

1. Add redisson-spring-boot-starter dependency into your project:

Maven

     <dependency>
         <groupId>org.redisson</groupId>
         <artifactId>redisson-spring-boot-starter</artifactId>
         <version>3.14.0</version>
     </dependency>

Gradle

     compile 'org.redisson:redisson-spring-boot-starter:3.14.0'

Downgrade redisson-spring-data module if necessary to support required Spring Boot version:

redisson-spring-data module nameSpring Boot version
redisson-spring-data-161.3.x
redisson-spring-data-171.4.x
redisson-spring-data-181.5.x
redisson-spring-data-202.0.x
redisson-spring-data-212.1.x
redisson-spring-data-222.2.x
redisson-spring-data-232.3.x
redisson-spring-data-242.4.x

2. Add settings into application.settings file

Common spring boot settings or Redisson settings could be used.

# common spring boot settings

spring:
  redis:
    database: 
    host:
    port:
    password:
    ssl: 
    timeout:
    cluster:
      nodes:
    sentinel:
      master:
      nodes:

  # Redisson settings
    
  #path to config - redisson.yaml
  redisson: 
    file: classpath:redisson.yaml
    config: |
      clusterServersConfig:
        idleConnectionTimeout: 10000
        connectTimeout: 10000
        timeout: 3000
        retryAttempts: 3
        retryInterval: 1500
        failedSlaveReconnectionInterval: 3000
        failedSlaveCheckInterval: 60000
        password: null
        subscriptionsPerConnection: 5
        clientName: null
        loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
        subscriptionConnectionMinimumIdleSize: 1
        subscriptionConnectionPoolSize: 50
        slaveConnectionMinimumIdleSize: 24
        slaveConnectionPoolSize: 64
        masterConnectionMinimumIdleSize: 24
        masterConnectionPoolSize: 64
        readMode: "SLAVE"
        subscriptionMode: "SLAVE"
        nodeAddresses:
        - "redis://127.0.0.1:7004"
        - "redis://127.0.0.1:7001"
        - "redis://127.0.0.1:7000"
        scanInterval: 1000
        pingConnectionInterval: 0
        keepAlive: false
        tcpNoDelay: false
      threads: 16
      nettyThreads: 32
      codec: !<org.redisson.codec.FstCodec> {}
      transportMode: "NIO"

3. Use Redisson through spring bean with RedissonClient interface or RedisTemplate/ReactiveRedisTemplate objects

8.各种锁的对比

方式优点缺点
数据库MYSQL的InnoDb实现简单,易于实现对数据库压力大
Redis易于理解自己实现、不支持阻塞
Zookeeper支持阻塞需理解Zookeeper、程序复杂
Curator【推荐】提供锁的方法依赖Zookeeper,强一致
Redission【推荐】提供锁的高级API,可阻塞

实际上,公司也是用Curator和Redisson的多,因为Redis集群和zk集群都是现成的,可以就地取材。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值