电商项目 - 分布式锁实现 - 编程式

分布式锁是什么?

互联网飞速发展,系统架构不断升级,从单体应用到集群。在单体应用当中,synchronized完全够用,但是集群下synchronized就无法起到作用了。这时分布式锁就出现了,分布式锁的本质和synchronized一致,都是为了多线程下一部分代码只能被一个线程执行。分布式锁市面上现在也有多种现有框架,实现方式有很多,实现原理大同小异。本篇文章将使用redis来实现分布式锁。

分布式锁实现原理

分布式锁实现原理

简单来说:

  • A线程加锁就是往redis中存入一个key,执行完删除key
  • B线程尝试获取锁redis存在当前key就证明锁目前有持有者;不存在则存入key获取锁成功

分布式锁实战

实现方式有很多。这里我们以Spring Boot为例,使用RedisTemplate实现。
测试用模拟下单扣减库存作为例子。

实现

代码结构图:
在这里插入图片描述
redis配置:

spring:
  redis:
    database: 1
    host: localhost
    port: 6379
    password: 123456
    timeout: 5000
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0

redis工具类(关键)
重点只需要关注 lock 和 unLock 方法

@Component
@Slf4j
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String LOCK_PREFIX = "lock_";


    /**
     * 获取锁。
     * 该获取锁方法有如下特性:
     * 1.如果获取锁成功,会设置锁的生存时间;
     * 2.虽然大多数情况下redis的锁都有生存时间,
     * 但是为了防止在上锁后、设置锁的生存周期
     * 之前获取锁的方法出现了异常而终止。我们加入如下判断:
     * 如果获取锁失败,会检查已存在锁是否设置有生存时间,
     * 如果没有设置生存时间,那么会给锁设置生存时间。
     *
     * @param lockName 锁名称
     * @return 如果获取锁成功则返回锁键对应值,否则返回null
     */
    public String lock(String lockName) {
        // 等待超时时间(可更灵活,这里为了简单理解,直接写死)
        long waitTimeOut = 1000 * 30;
        // 锁生存时间(可更灵活,这里为了简单理解,直接写死)
        int lockTimeOut = 30;
        String lockKey = LOCK_PREFIX + lockName;
        String lockId = UUID.randomUUID().toString();
        long end = System.currentTimeMillis() + waitTimeOut;
        try {
            // 等待锁未超时
            while (System.currentTimeMillis() < end) {
                if (setIfAbsent(lockKey, lockId)) {
                    // 成功获得锁 设置生存时间
                    expire(lockKey, lockTimeOut);
                    log.info("----------> 生成锁:" + lockId);
                    return lockId;
                }
                // 未获得锁
                if (exists(lockKey) && notSetExpire(lockKey)) {
                    // 避免获取锁的方法出现了异常被终止导致死锁,如果没有设置生存时间,那么会给锁设置生存时间
                    expire(lockKey, lockTimeOut);
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }

        } catch (Exception e) {
            log.error("lock error ------->", e);
            unLock(lockKey, lockId);
        }
        // 等待锁超时
        return null;
    }

    /**
     * 解锁。
     * 解锁时将判断锁键对应值是否是给定的值,防止误解锁。
     *
     * @param lockName 锁名称
     * @param lockId   锁键对应值
     * @return true如果解锁成功,否则返回false
     */
    public void unLock(String lockName, String lockId) {
        String lockKey = LOCK_PREFIX + lockName;
        if (get(lockKey) != null && get(lockKey).equals(lockId)) {
            // 删除锁
            del(lockKey);
            log.info("----------> 成功删除锁:" + lockId);
        }
    }

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        try {
            return redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(Arrays.asList(key));
            }
        }
    }

    /**
     * setNX 不存在存入
     *
     * @param key
     * @param value
     * @return
     */
    public boolean setIfAbsent(String key, String value) {
        return redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 从redis中获取key对应的过期时间;
     *
     * @param key
     * @return 如果该值有过期时间,就返回相应的过期时间;
     * 如果该值没有设置过期时间,就返回-1;
     * 如果没有该值,就返回-2;
     */
    public long expire(String key) {
        return redisTemplate.opsForValue().getOperations().getExpire(key);
    }


    /**
     * 判断key是否过期
     *
     * @param key
     * @return
     */
    public boolean notSetExpire(String key) {
        return expire(key) == -1;
    }


    /**
     * 判断key是否存在
     *
     * @param key
     * @return
     */
    public boolean exists(String key) {
        return redisTemplate.hasKey(key);
    }


}

创建OrderService

public interface OrderService {
    /**
     * 生成订单
     */
    void generateOrder();
}

创建OrderService实现类

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 模拟库存
     */
    private int stock = 0;

    /**
     * 生成订单
     */
    @Override
    public void generateOrder() {
        // 不加锁测试
        deductStock();
        /*
        // 加锁测试
        // .....
        // 扣减库存
        String lockKey = "lock_generateOrder";
        String lockId = redisUtil.lock(lockKey);
        if (Objects.isNull(lockId)) {
            return;
        }
        try {
            deductStock();
        } catch (Exception e) {
            log.error("generateOrder error", e);
        } finally {
            redisUtil.unLock(lockKey, lockId);
        }
        // .....
         */
    }

    private void deductStock() {
        stock = --stock;
        System.out.println(stock);
    }
}

创建OrderController

@RestController
public class OrderController {

    @Autowired
    OrderService orderService;

    @GetMapping("/generateOrder")
    public void generateOrder() {
        orderService.generateOrder();
    }
}
测试情况

这里测试只是为了证实锁是否有作用,测试例子并非符合实际业务场景。不用纠结,重点以理解分布式锁为主。

由于没有加分布式锁,并发下单1000次,最后输出的stock应该为-1000

测试实际输出:-925
在这里插入图片描述
这不是预期的结果。

将生成订单实现加锁:

/**
     * 生成订单
     */
    @Override
    public void generateOrder() {
        // 不加锁测试
        // deductStock();
        
        // 加锁测试
        // .....
        // 扣减库存
        String lockKey = "lock_generateOrder";
        String lockId = redisUtil.lock(lockKey);
        if (Objects.isNull(lockId)) {
            return;
        }
        try {
            deductStock();
        } catch (Exception e) {
            log.error("generateOrder error", e);
        } finally {
            redisUtil.unLock(lockKey, lockId);
        }
        // .....
    }

再次测试并发下单1000次,最后输出的stock为-1000
在这里插入图片描述

这就是我们预期的结果。

说明下,虽然测试用的单实例,集群下锁同样生效。虽然单实例下直接使用synchronized能达到更好的锁效果,但本文主要的目的是为了讲解分布式锁的实现。

分布式锁的实现方式有很多,理解思想使用任何中间件都能实现分布式锁。只是性能好不好的问题。

下篇《电商项目 - 分布式锁实现 - 声明式》

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值