0
点赞
收藏
分享

微信扫一扫

Zookeeper(一)分布式理论

1、基本原理和实现方式对比

分布式锁:满足分布式系统或集群模式下多个进程可见并且互斥的锁。分布式锁的核心思想就是多线程都使用同一把锁,实现程序串行执行。
1653374296906.png
分布式锁需要具备的条件:
1653381992018.png

特性含义
可见性多个线程都能感知到变化
互斥性分布式锁的最基本的特性,让程序串行执行
高可用程序不易崩溃,时刻保证较高的可用性
高性能要求分布式锁具备较高的加锁和释放锁性能
安全性要求分布式锁具备一定的安全性

常见的分布式锁有三种:
Mysql: mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见
Redis: redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
Zookeeper: zookeeper也是企业级开发中较好的一个实现分布式锁的方案,这里不过多阐述。
1653382219377.png

2、Redis分布式锁实现的核心思路

实现分布式锁需要实现的两个基本方法:

  • 获取锁
    • 互斥:只能有一个线程成功获取到锁
    • 非阻塞:尝试获取一次,成功返回true,失败返回false
  • 释放锁
    • 手动释放
    • 超时释放:避免服务宕机导致出现死锁

核心思路:利用redis的setnx特性实现锁的互斥。当第一个线程setnx返回1,代表它获取锁成功,可以执行业务,然后释放锁;其他线程则等待一段时间后进行重试。
image.png

3、实现分布式锁 V1.0

  • 锁对象接口
public interface ILock {

    /**
   * 尝试获取锁
   * @param timeoutSec 超时时间(秒)
   * @return
   */
    boolean tryLock(long timeoutSec);

    /**
   * 释放锁
   */
    void unlock();
}
  • 锁对象实现类
public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;

    // 锁的名字(一般与当前业务模块相关)
    private String name;
    private String LOCK_PREFIX = "lock:";

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        // value建议设置当前线程的id
        long threadId = Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // 不要直接返回success,自充拆箱可能会出现空指针异常
        return BooleanUtil.isTrue(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(LOCK_PREFIX + name);
    }
}
  • 业务类-VoucherOrderServiceImpl

核心代码:

// 使用分布式锁实现一人一单
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
// 尝试获取锁
boolean isLock = lock.tryLock(1200);
if (!isLock) {
    return Result.fail("不允许重复下单");
}
try {
    return oneUserAndOrder(voucherId);
} finally {
    lock.unlock();
}
/**
 * 一人一单
 *
 * @param voucherId
 * @return
 */
@Transactional
/*
    1、将锁放在方法体上,那么这个方法就是一个同步方法,只有一个线程能够进入,会导致性能问题
 */
public /*synchronized */Result oneUserAndOrder(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    /*
        2、将锁放在方法体内存在的问题:方法执行完毕后,锁会被释放,但事务是由Spring管理的
        此时,事务还未提交,锁就被释放了,下一个进程进来,仍会出现线程安全问题
     */
//        synchronized (userId.toString().intern()){
    // 保证一人一单
    Integer count = query().eq("voucher_id", voucherId)
            .eq("user_id", userId).count();
    if (count > 0) {
        return Result.fail("id为:" + userId + "的用户已经购买过该秒杀券");
    }

    // 扣减库存,添加乐观锁
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock - 1")
            .eq("voucher_id", voucherId)
            // 这种方式反而会增加下单的失败率
//                .eq("stock", voucher.getStock())
            // 只要我库存还大于0,就允许用户继续下单
            .gt("stock", 0)
            .update();
    if (!success) {
        return Result.fail("秒杀券已售罄");
    }
    // 生成订单
    VoucherOrder order = new VoucherOrder();
    long orderID = redisIdWorker.nextId("order");
    order.setId(orderID);
    order.setVoucherId(voucherId);
    order.setUserId(UserHolder.getUser().getId());
    save(order);
    return Result.ok(orderID);
}
  • 单元测试

image.png
image.png

4、分布式锁误删问题

4.1、误删问题

现考虑一种在分布式锁情况下仍会导致线程安全问题的极端情况:

  1. 线程1获取锁,获取成功,但因业务阻塞问题,导致分布式锁的TTL过期,锁失效
  2. 线程2获取锁,获取成功。
  3. 线程1执行完业务,释放锁,也就是把线程2的锁给释放掉了。
  4. 线程3获取锁,获取成功。
  5. 线程2执行完业务,释放锁,也就是释放了线程3的锁
  6. 线程3执行完业务,执行释放锁。

这种情况下,线程2和线程3存在线程安全问题。
导致该问题出现的本质原因在于线程在去释放锁的时候,不加判断,都不看这锁是不是自己的就给人家释放了。

4.2、解决方案

分布式锁会被误删的关键是redis再去删除数据的时候,没有做判断,当前线程没有判断在redis中存储的锁是不是自己的那把锁就直接给删掉了。
解决方案:给锁添加唯一标识(UUID),删除前做一次查询,判断是不是自己的那把锁,如果是,再做删除操作。

  • 核心代码更新

获取锁
image.png
删除锁
image.png

  • 测试

准备两个线程
image.png
线程1成功获取锁
image.png
image.png
通过手动删除锁,模拟线程1因业务阻塞导致锁过期被删除
线程2成功获取锁
image.png
线程1执行完业务,删除锁
image.png
线程2执行完业务,删除锁
image.png

5、分布式锁的原子性问题

5.1、原子性问题

目前仍存在一种更为极端的情况会导致分布式锁误删问题

  1. 线程1正常获取锁,执行业务逻辑,执行完毕准备删除锁
  2. 经过判断的确是自己的锁,此事发生线程阻塞等意外导致分布式锁TTL到期
  3. 线程2进入,获取到锁
  4. 切回到线程1,由于之前已经判断过是自己的锁了,直接执行释放锁操作

由此造成了分布式锁的误删问题
造成该问题出现的本质原因是:释放锁的查询判断和删除操作不具备原子性

5.2、通过Lua脚本解决原子性问题

Lua 是一种轻量级的编程语言,具有简洁的语法和强大的功能。它是一种动态类型的语言,支持函数式编程和面向对象编程。Lua 是一种嵌入式脚本语言,可以轻松地集成到其他应用程序中。
Redis提供了对Lua的支持实现
Spring提供了调用Lua脚本的API
基于这些特性,保证分布式锁删除操作原子性的实现思路:

  1. 将锁查询及删除操作写入到Lua脚本;
  2. 通过Spring调用编写好的Lua脚本

由于在Java中只有调用Lua脚本这一行操作语句,从而保证了原子性

  • unlock.lua
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
  • 释放锁核心代码
public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;

    // 锁的名字(一般与当前业务模块相关)
    private String name;
    private String LOCK_PREFIX = "lock:";
    final String uniqueStr = UUID.randomUUID().toString(true) + "-";

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // value建议设置当前线程的id
        long threadId = Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name, uniqueStr + threadId, timeoutSec, TimeUnit.SECONDS);
        // 不要直接返回success,自充拆箱可能会出现空指针异常
        return BooleanUtil.isTrue(success);
    }

    /**
     * 通过Lua脚本释放锁,保证操作的原子性
     */
    @Override
    public void unlock() {
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(LOCK_PREFIX + name), uniqueStr + Thread.currentThread().getId());
    }


//    @Override
//    public void unlock() {
//        // 查询当前线程的锁
//        String lock = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);
//        // 如果当前线程的锁是自己的,才能删除
//        if (lock != null && lock.equals(uniqueStr + Thread.currentThread().getId())){
//            stringRedisTemplate.delete(LOCK_PREFIX + name);
//        }
//    }
}
举报

相关推荐

0 条评论