学习分布式锁笔记

1.使用JVM锁来解决超卖问题(mysql模式)

entity

@Data
public class Stock {
    // 固定5000库存
    private Integer stock = 5000;
    
}

service:使用synchronized加锁

@Service
public class StockService {


    private Stock stock = new Stock();


    public synchronized void deduct() {
        stock.setStock(stock.getStock() - 1);
        System.out.println("库存余量" + stock.getStock());
        
    }
}

controller:

@RestController
@RequestMapping("stock")
public class StockController {


    @Autowired
    private StockService stockService;

    @GetMapping("deduct")
    public String deduct(){
        stockService.deduct();
        return "hello stock deduct";
    }
}

使用JMeter压力测试

刚好是0

 

 使用 ReentrantLock 加锁

@Service
public class StockService {


    private Stock stock = new Stock();

    private static ReentrantLock reentrantLock = new ReentrantLock();

    public  void deduct() {

        reentrantLock.lock();
        try {
            stock.setStock(stock.getStock() - 1);
            System.out.println("库存余量" + stock.getStock());
        } finally {
            reentrantLock.unlock();
        }

    }
}

2.问题:以上jvm锁有三种情况锁会失效

1. 多例模式

2. 事务

A请求B请求
开启事务开启事务
获取锁
查询库存:5000
扣减库存:4999
释放锁
获取锁

查询库存:5000

(因为A还没有提交事务)

提交事务
扣减库存:4999
释放锁
提交事务

这样下来,两个请求只减了一个库存

3. 集群部署(类似多例模式)

3.解决以上锁失效问题:

1. 利用mysql的原子性更新

update db_stock set count = count - #{count} where product_code = #{productCode} and count >= #{count}

问题:

1) 锁范围:

  • product_code 字段没有索引,就会产生表锁
  • 如果导致索引失效,都会走表锁

2) 同一个商品有多条记录问题

3) 无法记录库存变化前后的状态

2. 使用悲观锁

先查询,每次查询如果命中索引都会产生行级锁,不读去读取其他事务的数据

 

 代码示例:

select * from db_stock where product_code = #{productCode} for update
 @Transactional(rollbackFor = Exception.class)
    public void deduct() {
        List<Stock> stocks = stockMapper.queryStock(new Stock().setProductCode("1001"));
        Stock stock = stocks.get(0);
        if (stock.getCount() > 0) {
            stock.setCount(stock.getCount() - 1);
            stockMapper.updateById(stock);
        }
    }

问题:

1) 性能问题

2) 死锁

A请求B请求
select * from db_stock where id = 1 for updateselect * from db_stock where id = 2 for update
接下来A也想去获取id = 2接下来B也想去获取id = 1
select * from db_stock where id = 2 for updateselect * from db_stock where id = 1 for update
这样A和B都产生了死锁问题

3) 操作上也要统一

select * from db_stock where id = 1 for update

如果 没有加 for update 扣减库存就会出现问题

select * from db_stock where id = 1

3. 使用乐观锁(时间戳 版本号 CAS机制)

给数据库加上version字段

每次做更新操作都要看看version是不是一样,一样就更新成功,不一样则失败

update update db_stock set count = count - #{count},version = version +1 where product_code = #{productCode} and version = #{oldVersion}

问题:

1) 性能低:重试机制导致浪费性能

2 ) 出现ABA问题

3 ) 读写分离乐观锁不可靠:在主从数据复制的时候,需要io操作高并发的情况下读取数据就会不一致

4 .redis解决超卖问题

1.redis乐观锁解决问题

public void deduct() {

        stringRedisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // 监听 stock
                operations.watch("stock");
                // 1.查询库存
                String s = operations.opsForValue().get("stock").toString();
                if (!StringUtil.isNullOrEmpty(s)) {
                    Integer num = Integer.valueOf(s);
                    // 2.判断库存是否充足
                    if (num > 0) {
                        // 开启事务
                        operations.multi();
                        // 3. 扣减库存
                        operations.opsForValue().set("stock", String.valueOf(--num));
                        // 执行事务
                        List exec = operations.exec();
                        if (CollectionUtils.isEmpty(exec)) {
                            try {
                                Thread.sleep(20);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            deduct();
                        }
                        return exec;
                    }
                }
                return null;
            }
        });
    }

问题:性能很差,如果A线程进来修改这个值,B线程在监听中,B线程如果执行事务,发现这个stock被修改过了,执行失败,不断修改不断失败,导致耗时比较长

5.使用分布式锁

1.基于redis实现分布式锁

public void deduct() {

        // 加锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
        // 如果失败,递归重试
        if (!lock) {
            try {
                Thread.sleep(20);
                this.deduct();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            try {
                // 1.查询库存
                String s = stringRedisTemplate.opsForValue().get("stock");
                // 2.判断库存是否充足
                if (!StringUtil.isNullOrEmpty(s)) {
                    Integer num = Integer.valueOf(s);
                    // 2.判断库存是否充足
                    if (num > 0) {
                        // 3. 扣减库存
                        stringRedisTemplate.opsForValue().set("stock", String.valueOf(--num));
                    }
                }
            } finally {
                // 释放锁
                stringRedisTemplate.delete("lock");
            }
        }
    }

while调用

public void deduct() {

        // 加锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
        // 如果失败,循环调用
        while (!lock) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 1.查询库存
            String s = stringRedisTemplate.opsForValue().get("stock");
            // 2.判断库存是否充足
            if (!StringUtil.isNullOrEmpty(s)) {
                Integer num = Integer.valueOf(s);
                // 2.判断库存是否充足
                if (num > 0) {
                    // 3. 扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--num));
                }
            }
        } finally {
            // 释放锁
            stringRedisTemplate.delete("lock");
            
        }
    }

问题:

1.以上代码会存在如果一台服务器宕机,没有执行到  stringRedisTemplate.delete("lock");会出现死锁问题,获取锁和添加过期时间必须是原子性操作

2.防误删:

  // 加锁
        // 如果失败,循环调用
        while (!stringRedisTemplate.opsForValue().setIfAbsent("lock", "111", 3, TimeUnit.SECONDS)) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 1.查询库存
            String s = stringRedisTemplate.opsForValue().get("stock");
            // 2.判断库存是否充足
            if (!StringUtil.isNullOrEmpty(s)) {
                Integer num = Integer.valueOf(s);
                // 2.判断库存是否充足
                if (num > 0) {
                    // 3. 扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--num));
                }
            }
        } finally {
            // 释放锁
            stringRedisTemplate.delete("lock");

        }

以上代码给lock加了个过期时间,但是业务可以需要比这个过期时间更长,然后这个死锁自动释放掉了,第二个请求在进来,第一个请求就执行stringRedisTemplate.delete("lock");把第二请求的锁给释放了

加个uuid:

  public void deduct() {

        String uuid = UUID.randomUUID().toString();
        // 加锁
        // 如果失败,循环调用
        while (!stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS)) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 1.查询库存
            String s = stringRedisTemplate.opsForValue().get("stock");
            // 2.判断库存是否充足
            if (!StringUtil.isNullOrEmpty(s)) {
                Integer num = Integer.valueOf(s);
                // 2.判断库存是否充足
                if (num > 0) {
                    // 3. 扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--num));
                }
            }
        } finally {
            // 判断是否是自己的锁
            if (uuid.equals(stringRedisTemplate.opsForValue().get("lock"))) {
                // 释放锁
                stringRedisTemplate.delete("lock");
            }
        }
    }

以上获取uuid和删除锁不是原子操作,还是会出现误删问题

使用lua脚本实现原子性

# script  lua脚本字符串
# numkeys key的参数
# key 以空格分割,下标从1开始
# arg 以空格分割,下标从1开始
eval script numkeys key [key ...] arg [arg ...]
eval "if 10>20 then return 10 else return 20 end" 0
输出:20

#2是key的个数那就是 10 20 是key,剩下就是arg
eval "if KEYS[1]>ARGV[1] then return KEYS[2] else return ARGV[2] end" 2 10 20 30 40
输出:40

eval "return {10 , 20, 30}" 0
输出:
10 20 30

# 用lua脚本设置值和获取
127.0.0.1:6379> eval "return redis.call('set','lock','11111226566')" 0
OK
127.0.0.1:6379> get lock
"11111226566"


127.0.0.1:6379> eval "return redis.call('set','lock','11111226566')" 0
OK
127.0.0.1:6379> eval "return redis.call('get','lock')" 0
"11111226566"

通过lua脚本释放锁保障原子性

 public void deduct() {

        String uuid = UUID.randomUUID().toString();
        // 加锁
        // 如果失败,循环调用
        while (!stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS)) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 1.查询库存
            String s = stringRedisTemplate.opsForValue().get("stock");
            // 2.判断库存是否充足
            if (!StringUtil.isNullOrEmpty(s)) {
                Integer num = Integer.valueOf(s);
                // 2.判断库存是否充足
                if (num > 0) {
                    // 3. 扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--num));
                }
            }
        } finally {

            // 使用lua脚本释放锁
            String script = "if redis.call('get' , KEYS[1]) == ARGV[1] " +
                    "then " +
                    "return redis.call('del' , KEYS[1]) " +
                    "else " +
                    "return 0 " +
                    "end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);

        }
    }

6.redis避免死锁问题(ReentranLock可重入锁的底层原理)

public void A(){

//1. 这里获去lock锁


//2. 调用B方法
B();


//3. 释放锁
}


public void A(){

//1. 这里获去lock锁

//3. 释放锁

}

 * A获取一个lock锁没有释放之前去调用 B,这时候获取lock锁,发现被A占着,所以一直在等待
 * 而代码也走不到 释放锁那里,所以导致死锁

可重入锁流程(加锁):

// 1.进入lock方法
reentrantLock.lock();


// 2. 继续进入lock
public void lock() {
        sync.lock();
 }

// 3. 发现lock是个抽象方法
  abstract void lock();

//4. 选非公平锁的实现
 static final class NonfairSync extends Sync {
      
        /**
         * CAS获取锁,如果没有其他占用锁(state == 0),设置当前线程是有锁线程
         * 否执行 acquire(1);
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        // 尝试获取非公平锁
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

   // 5. 执行 acquire()方法
   public final void acquire(int arg) {
        if (!tryAcquire(arg) && // 这一行会 调用 NonfairSync中的tryAcquire方法,返回false代表 
                                //被占用
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

   // 6. 调用 NonfairSync中的tryAcquire方法里面的nonfairTryAcquire()
    final boolean nonfairTryAcquire(int acquires) {
           // 获取当前线程
            final Thread current = Thread.currentThread();
            int c = getState();
            //如果等于0,代码没有锁,则占用锁
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果不等于0,则线程相等 , state 相加
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

/**
* 7.如果第6步返回的是false,则执行第5步的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 这 * 
* 一行代码
**/
//这一步就是往队列后面加一个节点
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }


  

解锁:

// 1.点击unlock()方法
reentrantLock.unlock();

//2. 点击 release()方法
    public void unlock() {
        sync.release(1);
    }

// 3. 如果解锁成功就返回true
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

// 4.第3步的 tryRelease(arg) ,每次进来都减1,减到0了并且是当前线程的,就释放锁
   protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

参照reentrantLock实现非公平锁

加锁lua脚本:

  1. 判断锁是否存在(exists),则直接获取锁 hset key field value
  2. 如果锁存在则判断是否是自己的锁,如果是自己的锁则重入
  3. 否则重试(reentrantLock是入队列): 递归 循环
-- 如果不存在或一存在 ,则设置key --
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1
then 
--给redis设置一个hash类型的值(不存在直接设置,存在则加1)--
   redis.call('hincrby',KEYS[1],ARGV[1],1)
--设置过期时间(如果是已存在的叫续期)--
   redis.call('expire',KEYS[1], ARGV[2])
   return 1
else 
 return 0
end

解锁lua脚本:

  1. 判断自己的锁是否存在,如果不存在,返回nil
  2. 如果自己的锁存在,则减1 ,判断减1后的值是否为0,为0则释放锁(del)并返回1
  3. 不为0,返回0

if redis.call('hexists',KEYS[1],ARGV[1])==0
   then
     return nil
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0
    then 
      return redis.call('del',KEYS[1])
else 
    return 0
end

自动续期:

  1. 定时执行如果key还存在,则自动续期,并返回1
  2. 如果不存在返回0
if redis.call('hexists' , KEYS[1] , ARGV[1]) == 1
then 
    return redis.call('expire' , KEYS[1] , ARGV[2])
else
    return 0
end

java代码:

package com.czt.lock;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 *   @author czt
 *   @create 2022-12-24 10:19
 *
 */

public class DistributeRedisLock implements Lock {

    private String locKName;

    private String uuid;

    // 默认30秒
    private long expire = 30;

    private StringRedisTemplate stringRedisTemplate;

    public DistributeRedisLock(String locKName, StringRedisTemplate stringRedisTemplate, String uuid) {
        this.locKName = locKName;
        this.stringRedisTemplate = stringRedisTemplate;
        // 保障唯一标识
        this.uuid = uuid + ":" + Thread.currentThread().getId();
    }


    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {

        try {
            return this.tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 加锁
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {

        if (time == -1) {
            this.expire = unit.toSeconds(time);
        }
        String script = "if redis.call('exists', KEYS[1]) == 0" +
                "then " +
                "    redis.call('hset', KEYS[1], ARGV[1], 1) " +
                "    redis.call('expire', KEYS[1], ARGV[2]) " +
                "    return 1 " +
                "elseif redis.call('hexists', KEYS[1], ARGV[1]) == 1" +
                "then" +
                "    redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
                "    redis.call('expire', KEYS[1], ARGV[2]) " +
                "    return 1 " +
                "else  " +
                "   return 0 " +
                "end";

        while (!this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(this.locKName), uuid, String.valueOf(this.expire))) {
            Thread.sleep(50);
        }
        // 自动续期功能
        this.renewExpire();
        return true;
    }


    /**
     * 自动续期
     */
    public void renewExpire() {
        String script = "if redis.call('hexists' , KEYS[1] , ARGV[1]) == 1 " +
                "then " +
                "    return redis.call('expire' , KEYS[1] , ARGV[2]) " +
                "else " +
                "    return 0 " +
                "end";

        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                Boolean execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(locKName), uuid, String.valueOf(expire));
                // 如果锁还存在,继续执行续期
                if (execute) {
                    renewExpire();
                }
            }
        }, this.expire * 1000 / 3);// 每3分之一的时间执行一次


    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {

        String script = "if redis.call('hexists',KEYS[1],ARGV[1])==0  then return nil elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 then  return redis.call('del',KEYS[1]) else  return 0 end";
        Long flag = this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(this.locKName), uuid);

        if (flag == null) {
            throw new IllegalMonitorStateException("不是你的锁");
        }

    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

工厂代码:

package com.czt.lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 *   @author czt
 *   @create 2022-12-24 10:26
 *
 */
@Component
public class DistributeRedisLockClient {

    @Autowired
    StringRedisTemplate stringRedisTemplate;


    private String uuid;

    public DistributeRedisLockClient() {
        this.uuid = UUID.randomUUID().toString();
    }

    public DistributeRedisLock getDistributeRedisLock(String lockName) {
        return new DistributeRedisLock(lockName, stringRedisTemplate, this.uuid);
    }
}

调用方法:

  public void deduct() { 
  DistributeRedisLock lock = distributeRedisLockClient.getDistributeRedisLock("lock");
        lock.lock();

        try {
            // 1.查询库存
            String s = stringRedisTemplate.opsForValue().get("stock");
            // 2.判断库存是否充足
            if (!StringUtil.isNullOrEmpty(s)) {
                Integer num = Integer.valueOf(s);
                // 2.判断库存是否充足
                if (num > 0) {
                    // 3. 扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--num));
                }
            }
        } finally {
            lock.unlock();
        }
}

 分布式锁总结:

  1. 独占排他:锁只能自己占有
  2. 防死锁: 给key添加过期时间;可重入
  3. 防误删:不是自己唯一标识的不能删除
  4. 原子性:使用lua脚本;加锁和过期时间保障原子性;判断和释放之间保障原子性
  5. 可重入:hash+lua脚本,唯一标识一样加1
  6. 自动续期:定时器自动续期过期时间

问题:

以上存在redis如果集群部署

A在主中获取了锁,然后这台主机挂了,还没有来得及同步到从里面去,根据哨兵机制,另外一个从机会变成主机,B请求去获取锁,可以获取到,导致锁机制失效

7.红锁算法RedLock

 8. reidsson分布式锁

https://github.com/redisson/redisson/wiki/

可重入锁

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

公平锁

RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

读写锁

不允许并发
不允许并发
允许并发
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

信号量

  public void semaphore() {
        RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
        semaphore.trySetPermits(3);
        try {
            semaphore.acquire();
            System.out.println("获取资源,出来业务");
            TimeUnit.SECONDS.sleep(10 + new Random().nextInt(10));
            System.out.println("释放资源" + Thread.currentThread().getName());

            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

闭锁

9.zookeeper 实现分布式锁

安裝

解压

tar -zxvf

 重命名

mv apache-zookeeper-3.7.1-bin zookeeper

到zookeeper下创建data目录存放zookeeper数据

修改配置文件

 

 启动

#启动
./zkServer.sh start

#查看是否启动
./zkServer.sh status

#进入客户端
./zkCli.sh

 

 

main

   public static void main(String[] args) throws InterruptedException {
        ZooKeeper zooKeeper = null;

        // 使用闭锁让zk先获取到链接在执行具体的业务
        CountDownLatch countDownLatch = new CountDownLatch(1);
        try {
            zooKeeper = new ZooKeeper("192.168.3.10:2181", 30000, event -> {

                Watcher.Event.KeeperState state = event.getState();

                // 获取到链接
                if (Watcher.Event.KeeperState.SyncConnected.equals(state) && Watcher.Event.EventType.None.equals(event.getType())) {
                    System.out.println("获取到链接" + event);
                    countDownLatch.countDown();
                } else if (Watcher.Event.KeeperState.Closed.equals(state)) {
                    System.out.println("关闭链接");
                } else {
                    System.out.println("节点事件:" + event);
                }

            });
            countDownLatch.await();
            // 创建节点
            //createNode(zooKeeper);
            // 查询节点
            selectNode(zooKeeper);
            // 更新节点
            //updateNode(zooKeeper);
            // 删除节点
            //delNode(zooKeeper);

            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭链接
            if (zooKeeper != null) {
                zooKeeper.close();
            }
        }
    }

 创建节点

public static void createNode(ZooKeeper zooKeeper) throws KeeperException, InterruptedException {
        /**节点新增 : 永久 临时 永久序列化 临时序列化**/
        //1. 永久节点
        //zooKeeper.create("/czt/test", // 必须要有/czt目录才可以创建后面的/test
        //        "hello zookeeper".getBytes(StandardCharsets.UTF_8),
        //        ZooDefs.Ids.OPEN_ACL_UNSAFE,
        //        CreateMode.PERSISTENT // 永久节点
        //);

        2. 临时节点 : 如果链接关闭了,临时节点就被删除了
        //zooKeeper.create("/czt/test2",
        //        "hello zookeeper".getBytes(StandardCharsets.UTF_8),
        //        ZooDefs.Ids.OPEN_ACL_UNSAFE,
        //        CreateMode.EPHEMERAL
        //);

        //3. 永久序列化节点
        //zooKeeper.create("/czt/test3",
        //        "hello zookeeper".getBytes(StandardCharsets.UTF_8),
        //        ZooDefs.Ids.OPEN_ACL_UNSAFE,
        //        CreateMode.PERSISTENT_SEQUENTIAL
        //);

        //4. 临时序列化节点
        zooKeeper.create("/czt/test4",
                "hello zookeeper".getBytes(StandardCharsets.UTF_8),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL
        );
        zooKeeper.create("/czt/test4",
                "hello zookeeper".getBytes(StandardCharsets.UTF_8),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL
        );
        zooKeeper.create("/czt/test4",
                "hello zookeeper".getBytes(StandardCharsets.UTF_8),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL
        );

    }

查询节点

  public static void selectNode(ZooKeeper zooKeeper) throws KeeperException, InterruptedException {

        // 1.查询判断节点是否存在
        Stat stat = zooKeeper.exists("/czt", false);
        if (stat == null) {
            System.out.println("当前节点不存在");
        } else {
            System.out.println("当前节点存在");
        }

        // 2.获取当前节点中的数据内容

        byte[] data = zooKeeper.getData("/czt", false, stat);
        System.out.println("当前节点内容:" + new String(data));
        // 3.获取当前节点的子节点 
        List<String> children = zooKeeper.getChildren("/czt", new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("字节点方法变化" + event);
            }
        });
        System.out.println("当前节点的子节点:" + children);

    }

更新节点

  public static void updateNode(ZooKeeper zooKeeper) throws KeeperException, InterruptedException {
        // 1.查询判断节点是否存在
        Stat stat = zooKeeper.exists("/czt", false);
        if (stat == null) {
            return;
        }

        // 更新 :版本号必须和当前节点的版本一直,否则更新失败,指定为-1,代表不关心版本号
        Stat updateStat = zooKeeper.setData("/czt", "hhhhhh.......".getBytes(StandardCharsets.UTF_8), stat.getVersion());

    }

删除节点

 public static void delNode(ZooKeeper zooKeeper) throws KeeperException, InterruptedException {
        // 1.查询判断节点是否存在
        Stat stat = zooKeeper.exists("/czt", false);
        if (stat == null) {
            return;
        }

        // 删除节点
        zooKeeper.delete("/czt/test", -1);

    }

 实现分布式锁

package com.czt.zk;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;

/**
 *   @author czt
 *   @create 2022-12-26 11:57
 *
 */
public class ZkDistributedLock implements Lock {


    private String lockName;

    private ZooKeeper zooKeeper;

    private String currentNodePath;

    private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();

    private static final String ROOT_LOCK_PATH = "/locks";

    public ZkDistributedLock(String lockName, ZooKeeper zooKeeper) {
        this.lockName = lockName;
        this.zooKeeper = zooKeeper;

        // 初始化根节点节点
        try {
            if (zooKeeper.exists(ROOT_LOCK_PATH, false) == null) {
                zooKeeper.create(ROOT_LOCK_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        try {

            // 判断可重入, +1 操作
            Integer flag = THREAD_LOCAL.get();
            if (flag != null && flag > 0) {
                THREAD_LOCAL.set(flag + 1);
                return true;
            }

            // 创建znode节点:为了防宕机,要用临时节点
            // 创建临时序列化节点
            currentNodePath = this.zooKeeper.create(ROOT_LOCK_PATH + "/" + this.lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            // 获取当前节点的前置节点,如果为空,则获取锁成功,否则监听前置节点
            String preNode = this.getPreNode();

            if (preNode != null) {
                // 利用闭锁实现阻塞功能
                CountDownLatch countDownLatch = new CountDownLatch(1);
                // 高并发下,这个锁被删除了,就会触发这个监听事件
                if (this.zooKeeper.exists(ROOT_LOCK_PATH + "/" + preNode, new Watcher() {
                    @Override
                    public void process(WatchedEvent event) {
                        countDownLatch.countDown();
                    }
                }) == null) {
                    THREAD_LOCAL.set(1);
                    return true;
                }
                countDownLatch.await();
            }
            THREAD_LOCAL.set(1);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    private String getPreNode() {
        try {
            // 获取根节点下面的所有节点
            List<String> children = this.zooKeeper.getChildren(ROOT_LOCK_PATH, false);
            if (CollectionUtils.isEmpty(children)) {
                throw new IllegalMonitorStateException("非法操作!");
            }

            // 获取当前节点同一资源的锁
            List<String> nodes = children.stream().filter(item -> item.startsWith(this.lockName + "-")).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(nodes)) {
                throw new IllegalMonitorStateException("非法操作!");
            }
            //排序
            Collections.sort(nodes);

            // 例如:/czt/test  获取到test
            // 获取当前节点
            String currentNode = StringUtils.substringAfterLast(currentNodePath, "/");
            // 获取当前节点下标
            int index = Collections.binarySearch(nodes, currentNode);
            if (index < 0) {
                throw new IllegalMonitorStateException("非法操作!");
            } else if (index > 0) {
                // 返回前置节点
                return nodes.get(index - 1);
            }
            // 如果当前节点就是第一个节点,则返回null
            return null;

        } catch (Exception e) {
            throw new IllegalMonitorStateException("非法操作!");
        }

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {


        return false;
    }

    @Override
    public void unlock() {
        try {
            // 可重入特性:每次都减1,到0了说明需要释放掉了
            THREAD_LOCAL.set(THREAD_LOCAL.get() - 1);
            if (THREAD_LOCAL.get() == 0) {
                this.zooKeeper.delete(currentNodePath, -1);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}
package com.czt.zk;

import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.CountDownLatch;

/**
 *   @author czt
 *   @create 2022-12-26 11:52
 *
 */
@Component
public class ZkClient {


    private ZooKeeper zooKeeper;

    @PostConstruct
    public void init() {

        // 使用闭锁让zk先获取到链接在执行具体的业务
        CountDownLatch countDownLatch = new CountDownLatch(1);
        try {
            zooKeeper = new ZooKeeper("192.168.3.10:2181", 30000, event -> {

                Watcher.Event.KeeperState state = event.getState();

                // 获取到链接
                if (Watcher.Event.KeeperState.SyncConnected.equals(state) && Watcher.Event.EventType.None.equals(event.getType())) {
                    System.out.println("获取到链接" + event);
                    countDownLatch.countDown();
                } else if (Watcher.Event.KeeperState.Closed.equals(state)) {
                    System.out.println("关闭链接");
                } else {
                    System.out.println("节点事件:" + event);
                }

            });
            countDownLatch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @PreDestroy
    public void destroy() {
        // 释放zk链接
        if (null != zooKeeper) {
            try {
                zooKeeper.close();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


    public ZkDistributedLock getLock(String lockName) {
        return new ZkDistributedLock(lockName, zooKeeper);
    }
}

总结:

  1. zk是公平锁: 序列化临时节点序号都是有序的
  2. 独占排他 :节点不重复
  3. 可重入:利用线程变量来控制可重入
  4. 阻塞:利用闭锁来阻塞
  5. 防止死锁:宕机临时节点也删除了,就不用过期时间了
  6. 防止误删:每个请求序列化的序号都是唯一的

10.Curator实现分布式锁


        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.4.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.4.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

config:

/**
 *   @author czt
 *   @create 2022-12-26 17:31
 *
 */
@Configuration
public class CuratorConfig {


    @Bean
    public CuratorFramework curatorFramework() {
        // 初始化一个重试策略,指数补偿策略
        RetryPolicy retry = new ExponentialBackoffRetry(10000, 3);
        // 连接zk,初始化curator客户端
        CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("192.168.3.10:2181", retry);
        // 手动启动
        curatorFramework.start();
        return curatorFramework;
    }
}

service:

  public void deduct() {

        InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/curator/locks");
        try {

            mutex.acquire();
            // 1.查询库存
            String s = stringRedisTemplate.opsForValue().get("stock");
            // 2.判断库存是否充足
            if (!StringUtil.isNullOrEmpty(s)) {
                Integer num = Integer.valueOf(s);
                // 2.判断库存是否充足
                if (num > 0) {
                    // 3. 扣减库存
                    stringRedisTemplate.opsForValue().set("stock", String.valueOf(--num));
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                mutex.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }


    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值