【实践经验】分布式锁实现与测试

通过本文你将学习到

  • 单机下的锁
  • 基于mysql分布式锁实现
  • 基于redis分布式锁实现
  • 常用的接口测试工具

我们通过商品超卖的场景来测试验证不同情景下的锁实现。

目录

单机

未加锁

加锁

集群

​编辑

单机加锁

分布式锁

数据库表锁

Redis的setNX实现

总结


单机

单机开发场景中涉及并发同步时,往往采用Synchronized(同步)或同一个JVM内Lock机制来解决多线程间的同步问题。

未加锁
    // 不加锁
    @GetMapping("/reduce")
    public Response reduce() {
        Good good = goodMapper.selectByPrimaryKey(1);
        if (good == null || good.getCount() < 1) {
            log.info("库存不足");
            return Response.handleFail("库存不足");
        }
        goodMapper.reduceGoodCount(1);
        log.info("扣除成功");
        return Response.success();
    }

 商品good表:初始状态商品1库存为2

使用ab(Apache Benchmark)进行测试

#发送200个请求,并发为200
ab -n 200 -c 200 http://127.0.0.1:8081/test/reduce

日志输出:

【实践经验】分布式锁实现与测试_第1张图片

可以看到扣除了多次,再看看库存结果,和明显超卖了不少:

加锁
    // 加锁
    @GetMapping("/reduce")
    public synchronized Response reduce() {
        Good good = goodMapper.selectByPrimaryKey(1);
        if (good == null || good.getCount() < 1) {
            //只观察扣除成功日志
            //log.info("库存不足");
            return Response.handleFail("库存不足");
        }
        goodMapper.reduceGoodCount(1);
        log.info("扣除成功");
        return Response.success();
    }

将库存更新为2后再次测试

#发送200个请求,并发为200
ab -n 200 -c 200 http://127.0.0.1:8081/test/reduce

日志只有两条:

库存也正常:

【实践经验】分布式锁实现与测试_第2张图片

集群

通过本地启动不同端口的两个进程模拟集群情况,用JMeter进行测试,添加两个线程组,每个线程组100个线程,并发请求两个进程下的相同接口模拟集群情况。

【实践经验】分布式锁实现与测试_第3张图片

【实践经验】分布式锁实现与测试_第4张图片

单机加锁
    // 加锁
    @GetMapping("/reduce")
    public synchronized Response reduce() {
        Good good = goodMapper.selectByPrimaryKey(1);
        if (good == null || good.getCount() < 1) {
            //只观察扣除成功日志
            //log.info("库存不足");
            return Response.handleFail("库存不足");
        }
        goodMapper.reduceGoodCount(1);
        log.info("扣除成功");
        return Response.success();
    }

初始库存:

执行日志:

【实践经验】分布式锁实现与测试_第5张图片

可以看到8081端口的扣除了4次,8082扣除了2次,很明显超买了1次,数据库库存为-1

【实践经验】分布式锁实现与测试_第6张图片

分布式锁

关于分布式锁这里我们带来两种简单的实现方案

  1. 数据库表锁
  2. Redis的setNX实现

其他更多更高级的欢迎评论区留言讨论

测试的并发条件和库存和单机一致,下面就不重复贴出。

数据库表锁
    @GetMapping("/reduce")
    public Response reduce()  throws Exception{
        Connection conn = dataSource.getConnection();
        boolean connAutoCommit =  conn.getAutoCommit();
        conn.setAutoCommit(false);
        PreparedStatement preparedStatement =
                conn.prepareStatement("select * from good_lock where name = 'good_lock' for update" );
        // 加锁
        preparedStatement.execute();
        Good good = goodMapper.selectByPrimaryKey(1);
        if (good == null || good.getCount() < 1) {
            // 提交释放锁
            release(conn, connAutoCommit, preparedStatement);
            return Response.handleFail("库存不足");
        }
        goodMapper.reduceGoodCount(1);
        // 提交释放锁
        release(conn, connAutoCommit, preparedStatement);
        log.info("扣除成功");
        return Response.success();
    }

    private static void release(Connection conn, 
                                boolean connAutoCommit, 
                                PreparedStatement preparedStatement) throws SQLException {
        conn.commit();
        conn.setAutoCommit(connAutoCommit);
        conn.close();
        preparedStatement.close();
    }
Redis的setNX实现

锁实现类

@Slf4j
public class RedisLock implements AutoCloseable {

    private RedisTemplate redisTemplate;
    private static final String KEY = "good_lock";
    private static final int EXPIRE_TIME = 30 * 1000;
    private String VALUE ;


    /**
     * 没有传递 value,因为直接使用的是随机值
     */
    public RedisLock(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
        this.VALUE = UUID.randomUUID().toString();
    }


    @Override
    public void close() throws Exception {
        unLock();
    }

    /**
     * 获取分布式锁
     * SET key value NX PX 30000
     * 每一个线程对应的随机值 key value 不一样,用于释放锁的时候校验
     * NX 表示 key 不存在的时候成功,key 存在的时候设置不成功
     * PX 表示过期时间
     */
    public boolean getLock(){
        RedisCallback redisCallback = connection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(EXPIRE_TIME* 1000);
            //序列化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;
    }

    /**
     * 释放锁的时候随机数相同的时候才可以释放,避免释放了别人设置的锁(自己的已经过期了所以别人才可以设置成功)
     * 释放的时候采用 LUA 脚本,因为 delete 没有原生支持删除的时候校验值,证明是当前线程设置进去的值
     * 脚本是在官方文档里面有的
     */
    public boolean unLock() {
        // key 是自己才可以释放,不是就不能释放别人的锁
        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 redisScript = RedisScript.of(script,Boolean.class);
        List keys = Arrays.asList(KEY);

        // 执行脚本的时候传递的 value 就是对应的值
        Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, VALUE);
        return result;
    }
}

业务方法

    @GetMapping("/reduce")
    public Response reduce()  throws Exception{
        // try-resource-close
        try (RedisLock lock =  new RedisLock(redisTemplate)) {
            // 获取锁
            if (lock.getLock()) {
                Good good = goodMapper.selectByPrimaryKey(1);
                if (good == null || good.getCount() < 1) {
                    return Response.handleFail("库存不足");
                }
                goodMapper.reduceGoodCount(1);
                log.info("扣除成功");
                return Response.success();
            }
        } catch (InterruptedException e) {
            log.error("e", e);
        } catch (Exception e) {
            log.error("e", e);
        }
        // 没有获取到锁
        return Response.handleFail("请重试");
    }

该锁情况下并发测试通过,测试case同数据库表锁。

总结

分布式集群情况下,无法通过单机的锁来避免高并发下的超买等问题,该篇文章场景是业务场景的减化版,并不能直接用于生产,所以使用的简单的分布式锁,在实际场景中还需要考虑事物,加锁的时机等。当然还有其他的分布式锁,欢迎评论区留言交流

你可能感兴趣的:(分布式)