分布式锁

1: 基于数据库实现分布式锁的步骤

 多个进程,多个线程 访问共同组件数据库

   通过select.........for update 访问同一条数据

   for update 锁定数据 其他线程只能等待

 优缺点:

     优点: 简单方便 易于理解 易于操作

     缺点: 并发量大时 对数据库压力较大

     建议: 作为锁的数据库与业务数据库分开

代码示例:

1: 端口8081 8080启动两个程序 

2: 访问8080 接口 日志--我进入了方法!我进入了锁!

3: 访问8081 接口 日志--我进入了方法!再往下需要等待8080释放锁

 
package com.example.distributelock.controller;

import com.example.distributelock.dao.DistributeLockMapper;
import com.example.distributelock.model.DistributeLock;
import com.example.distributelock.model.DistributeLockExample;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@RestController
@Slf4j
public class DemoController {
    @Resource
    private DistributeLockMapper distributeLockMapper;

    @RequestMapping("singleLock")
// 必须加上事务 否则第一个线程进去了 访问完直接就提交了 第二个线程依旧能进去 需要加上事物 在方法执行完成之后再提交 @Transactional(rollbackFor
= Exception.class) public String singleLock() throws Exception { log.info("我进入了方法!"); DistributeLock distributeLock = distributeLockMapper.selectDistributeLock("demo"); if (distributeLock==null) throw new Exception("分布式锁找不到"); log.info("我进入了锁!"); try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); } return "我已经执行完成!"; } }

-- sql中加锁的语句
selectDistributeLock
<select id="selectDistributeLock" resultType="com.example.distributelock.model.DistributeLock">
select * from distribute_lock
where business_code = #{businessCode,jdbcType=VARCHAR}
for update
</select>

 

2: 基于Redis的Setnx实现分布式锁的步骤

 my_random_value: 随机值,每个线程的随机值都不同,用于释放锁时的校验

 nx: key不存在时设置成功,key存在时设置不成功

 px:自动失效时间,出现一场情况,锁可以过期失效

 

实现原理:

 利用nx的原子性,多个线程并发时,只有一个线程可以设置成功 -- Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系

 设置成功即获得锁,可以执行后续的业务处理

  如果出现异常,过了锁的有效期,锁自动释放

  释放锁采用dedis的delete命令

  释放锁时校验之前设置的随机数,相同才释放

  释放锁的lua校本

代码案例:
package com.example.distributelock.controller;

import com.example.distributelock.lock.RedisLock;
import com.example.distributelock.lock.ZkLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class RedisLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("redisLock")
    public String redisLock(){
        log.info("我进入了方法!");
        try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
            if (redisLock.getLock()) {
                log.info("我进入了锁!!");
                Thread.sleep(15000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
//unlock();释放锁放在了RedisLocl的自动close下面 jdk的AutoCloseable特性 log.info(
"方法执行完成"); return "方法执行完成"; } } package com.example.distributelock.lock; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.data.redis.core.types.Expiration; import java.util.Arrays; import java.util.List; import java.util.UUID; @Slf4j public class RedisLock implements AutoCloseable { private RedisTemplate redisTemplate; private String key; private String value; //单位:秒 private int expireTime; public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){ this.redisTemplate = redisTemplate; this.key = key; this.expireTime=expireTime; this.value = UUID.randomUUID().toString(); } /** * 获取分布式锁 * @return */ public boolean getLock(){ RedisCallback<Boolean> redisCallback = connection -> { //设置NX RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent(); //设置过期时间 Expiration expiration = Expiration.seconds(expireTime); //序列化key byte[] redisKey = redisTemplate.getKeySerializer().serialize(key); //序列化value byte[] redisValue = redisTemplate.getValueSerializer().serialize(value); //执行setnx操作 Boolean result = connection.set(redisKey, redisValue, expiration, setOption); return result; }; //获取分布式锁 Boolean lock = (Boolean)redisTemplate.execute(redisCallback); return lock; } public boolean unLock() { String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class); List<String> keys = Arrays.asList(key); Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value); log.info("释放锁的结果:"+result); return result; } @Override public void close() throws Exception { unLock(); } }

为什么释放锁时候需要lua脚本

答:

在多线程情况下,你的线程A 执行了unlock,也判断完,这个时候恰恰在你执行到redisDao.delete(key),还未执行的时候,失去了CPU执行权。这个时间 + 你执行业务代码的时间大于5秒。也就是说,这个时候线程A拿到的锁已经被释放掉。 这个时候线程B拿到CPU执行权,并且执行了lock的逻辑(线程A设置的过期时间过期了,线程B获取到了锁),并且成功,然后恰巧,线程B在这个时候失去了CPU执行权,线程A又获得了执行权(线程A锁过期了,但是程序还需要继续执行,执行到最后再次释放锁),并且成功的执行了redisDao.delete(key),(线程A和线程B的key相同), 这个时候线程A就把线程B获取的锁删除了,这个时候就会出现坏情况啦。

没错,你看似有这么多凑巧,但是在多线程情况下都是可能出现的。 如果你在执行redisDao.delete(key)的时候,给上value,redisDao.delete(key,uniqueValue),在删除的时候就会判断value是否相等, 就不会出现线程A删除线程B锁的情况了。 (每次代码执行结束释放锁的时候,判断redis里面的value是否与前面设置的value值是否相等,相等在释放,如果查询到的value是空,说明本次锁已经超时了,但是为了防止代码异常,释放锁操作应该放到finnlly里面)

在执行redisDao.delete(key,uniqueValue)的时候,delete命令不是原子性的,比如在delete的过程中,在此过来一个线程c,重新设置了一个redis锁,会直接将线程c的锁删除掉,而LUA执行脚本的时候就一个线程,本身是具有原子性的.(将判断value值相等,delete操作都放在lua脚本中)

 

推荐文章: https://www.jianshu.com/p/2a2f3d133e8d

3: 基于Zookeeper实现分布式锁的步骤

设置观察器的三个方法: getData(),getChildren(),exists();

节点数据发生变化,发送给客户端

观察器只能观察一次,在监控需重新设置

 

实现原理:

利用zookeeper的瞬时有序节点的特性

多线程并发创建瞬时阶段节点时,得到有序的序列

序号最小的线程获得锁

其他线程监听自己序号的前一个序号节点

前一个线程执行完成,删除自己的序号节点

下一个序号的线程得到通知,继续执行

创建节点时,就已经决定了线程的执行顺序

 

代码示例:

package com.example.distributezklock.controller;

import com.example.distributezklock.lock.ZkLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

@RestController
@Slf4j
public class ZookeeperController {
    @Autowired
    private CuratorFramework client;

    @RequestMapping("zkLock")
    public String zookeeperLock(){
        log.info("我进入了方法!");
        try (ZkLock zkLock = new ZkLock()) {
            if (zkLock.getLock("order")){
                log.info("我获得了锁");
                Thread.sleep(10000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("方法执行完成!");
        return "方法执行完成!";
    }
}



package com.example.distributezklock.lock;

import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

@Slf4j
public class ZkLock implements AutoCloseable, Watcher {

    private ZooKeeper zooKeeper;
    private String znode;

    public ZkLock() throws IOException {
        this.zooKeeper = new ZooKeeper("localhost:2181",
                10000,this);
    }

    public boolean getLock(String businessCode) {
        try {
            //创建业务 根节点
            Stat stat = zooKeeper.exists("/" + businessCode, false);
            if (stat==null){
                zooKeeper.create("/" + businessCode,businessCode.getBytes(),
                        ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.PERSISTENT);
            }

            //创建瞬时有序节点  /order/order_00000001
            znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            //获取业务节点下 所有的子节点
            List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
            //子节点排序
            Collections.sort(childrenNodes);
            //获取序号最小的(第一个)子节点
            String firstNode = childrenNodes.get(0);
            //如果创建的节点是第一个子节点,则获得锁
            if (znode.endsWith(firstNode)){
                return true;
            }
            //不是第一个子节点,则监听前一个节点
            String lastNode = firstNode;
            for (String node:childrenNodes){
                if (znode.endsWith(node)){
                    zooKeeper.exists("/"+businessCode+"/"+lastNode,true);
                    break;
                }else {
                    lastNode = node;
                }
            }
            synchronized (this){
                wait();
            }

            return true;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }





    @Override
    public void close() throws Exception {
        zooKeeper.delete(znode,-1);
        zooKeeper.close();
        log.info("我已经释放了锁!");
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeDeleted){
            synchronized (this){
                notify();
            }
        }
    }
}

 

基于zookeeper下提供的curator实现分布式锁 --curator已经实现了分布式锁 直接调用即可

package com.example.distributezklock.controller;

import com.example.distributezklock.lock.ZkLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

@RestController
@Slf4j
public class ZookeeperController {
    @Autowired
    private CuratorFramework client;

    @RequestMapping("curatorLock")
    public String curatorLock(){
        log.info("我进入了方法!");
        InterProcessMutex lock = new InterProcessMutex(client, "/order");
        try{
            if (lock.acquire(30, TimeUnit.SECONDS)){
                log.info("我获得了锁!!");
                Thread.sleep(10000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                log.info("我释放了锁!!");
                lock.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        log.info("方法执行完成!");
        return "方法执行完成!";
    }
}

//需要初始化CuratorFrameword这个bean
@Bean(initMethod="start",destroyMethod = "close")
    public CuratorFramework getCuratorFramework() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
        return client;
    }

 

4: 基于Redisson实现分布式锁

https://zhuanlan.zhihu.com/p/135864820

--引入其他的人博客 感觉写的不错

 

 

 

 
 
posted @ 2022-05-02 21:09  那一年,我二十二  阅读(59)  评论(0)    收藏  举报