<返回更多

分布式锁的实现方式

2022-05-06    邬小明
加入收藏

1. 为什么需要锁

在并发场景下,多个进程/线程同时对同一个资源进行访问时,会产生冲突。
举个例子:核酸采样时,如果一次100个人同时要求大白进行采样(并发),那么大白就要崩溃了,所以必须要控制一个大白一次只能对一个人采样,其他人等待采样完成,这就是对大白进行”加锁”。

2. 锁是用来解决什么问题的

锁是用来解决并发问题的,如:

锁的使用场景:

3. 分布式锁的解决方案

4 通过Redis实现分布式锁示例

通过JAVA程序连接Redis,提示以下错误:

redis.clients.jedis.exceptions.JedisConnectionException: Failed to connect to any host resolved for DNS name.
    at redis.clients.jedis.DefaultJedisSocketFactory.connectToFirstSuccessfulHost(DefaultJedisSocketFactory.java:63)
    at redis.clients.jedis.DefaultJedisSocketFactory.createSocket(DefaultJedisSocketFactory.java:87)
    at redis.clients.jedis.Connection.connect(Connection.java:180)
    at redis.clients.jedis.Connection.initializeFromClientConfig(Connection.java:338)

可能有如下原因:

4.1 配置允许远程连接Redis

4.1.1 开放Redis端口(6379)

//查看6379端口状态 mo表示未开放
[root@192 bin]# firewall-cmd --zone=public --query-port=6379/tcp 
no
//配置放行6379端口
[root@192 bin]# firewall-cmd --zone=public --add-port=6379/tcp --permanent
success
//防火墙重载
[root@192 bin]#  firewall-cmd --reload
success
//再次查看端口状态
[root@192 bin]# firewall-cmd --zone=public --query-port=6379/tcp
yes

4.1.2 修改redis.conf配置文件

[root@192 redis]# vim redis.conf

将配置改成如下所示:

# 允许任何主机连接、访问
bind 0.0.0.0
# 关闭保护模式
protected-mode no
# 允许启动后在后台运行,即关闭命令行窗口后仍能运行
daemonize yes

4.2 模拟秒杀下单减库存的场景

4.2.1 新建以下商品表(t_goods)与订单表(t_order)

DROP TABLE IF EXISTS `t_goods`;
CREATE TABLE `t_goods` (
  `id` int(11) NOT NULL,
  `name` varchar(60) DEFAULT NULL,
  `qty` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--初始化数量qty=20
INSERT INTO `t_goods` VALUES ('1', '华为nova7', '20');
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
  `oid` varchar(120) NOT NULL,
  `createtime` datetime DEFAULT NULL,
  `goodname` varchar(255) DEFAULT NULL,
  `user` varchar(120) DEFAULT NULL,
  PRIMARY KEY (`oid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4.2.2 编写main方法,同时启动10000条线程模拟秒杀

    public static void main(String[] args) throws ParseException {

        //设置秒杀开始时间
        String startTime="2022-4-27 23:24:00";
        SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date start=simpleDateFormat.parse(startTime);
        System.out.println("等待中...");
        boolean isStart=false;
        while (!isStart) {
            if (start.compareTo(new Date()) < 0) {
                isStart=true;
                System.out.println("秒杀开始...");
                //同时启动10000条线程
                CountDownLatch countDownLatch = new CountDownLatch(10000);
                for (int i = 0; i < 10000; i++) {
                    new Thread(() -> {
                        try {
                            countDownLatch.await();
                            //secKillGoodsByRedisLock();//Redis锁
                            secKillGoods();  //未加锁
                           // secKillGoodsByLock();//synchronized

                        } catch (InterruptedException | SQLException e) {
                            e.printStackTrace();
                        }
                    }).start();
                    countDownLatch.countDown();
                }
            }
        }
    }

4.2.3 未加锁的情况

private static void secKillGoods() throws SQLException {
    List list = DButil.query("select id,name,qty from t_goods where id=1 and qty>0");
    //判断是否还有库存
    if(list.size()>0){
        String id=JedisUtil.getId();
        Object[] insertObject={id,new Date(),"nova7",Thread.currentThread().getName()};
        DButil.excuteDML("update t_goods set qty=qty-1 where id=1 ");
        DButil.excuteDML("insert into t_order values(?,?,?,?);",insertObject);
        System.out.println(Thread.currentThread().getName()+">>>抢到了."+id);
    }
}

以上代码每次执行都会先判断是否还有库存,如果有库存则秒杀成功,否则秒杀失败,没有并发的情况下是可以正常运行的,但是一旦存在并发,则会出现负库存(超卖)

分布式锁的实现方式

 

4.2.4 通过synchronized关键字对代码块加锁

private static void secKillGoodsByLock() throws SQLException {
    synchronized (lockObj) {
        secKillGoods();
    }
}

以上程序在进入秒杀方法时,都会通过synchronized关键字加锁,再次运行程序,我们发现不会出现负库存了

分布式锁的实现方式

 

但是如果在多进程或者分布式环境中,synchronized关键字会失效,让我们再启动一个进程,两个进程同时启动100000个线程进行秒杀(ps:同时启动十万个线程差点让我的电脑没缓过来…),终于出现了负库存的现象

分布式锁的实现方式

 

4.2.5 Redis锁

private static void secKillGoodsByRedisLock(){
        String id="1";
        String key="lock"+id;
        JedisUtil jedisUtil=new JedisUtil();
        String lockId=jedisUtil.getLock(key,5);

        if(null!=lockId){
            try{
                List list= DButil.query("select id,name,qty from t_goods where id=1 and qty>0");
                if(list!=null && list.size()>0){
                    Object[] insertObject={lockId,new Date(),"nova7",Thread.currentThread().getName()};
                    DButil.excuteDML("update t_goods set qty=qty-1 where id=1 ");
                    DButil.excuteDML("insert into t_order values(?,?,?,?);",insertObject);
                    System.out.println(Thread.currentThread().getName()+">>>抢到了."+lockId);
                    jedisUtil.unLock(key,lockId);

                }else {
                    System.out.println("抢完了");
                }
            } catch (SQLException e) {
                jedisUtil.unLock(key,lockId);
                e.printStackTrace();
            }
        }


    }

以上代码只有在获取到Redis锁成功后,才会去执行扣库存和下单的逻辑,重复和上一步一样,两个进程同时启动100000个线程进行秒杀,看看结果

分布式锁的实现方式

 


分布式锁的实现方式

 

以上结果没有出现负库存的现象,显然是扛住了“秒杀”,getLock 的实现如下所示,其原理就是利用Redis setnx的原子性操作来控制并发,以下示例还设置了锁失效的时间,避免死锁。当然还有许多的问题需要在实际应用场景中考虑,如在锁失效时间到了,秒杀动作未完成如何处理,Redis服务器崩溃了怎么办等等。


    public String getLock(String key,int timeout){
        Jedis jedis=null;
        try {
            jedis=getJedis();
            String value=getId();
            long end=System.currentTimeMillis()+timeout;
            while (System.currentTimeMillis()<end) {
                //设置value成功,获取锁
                if(jedis.setnx(key,value)==1){
                    //设置失效时间
                    jedis.expire(key,timeout);
                    System.out.println(Thread.currentThread().getName()+">>>>>获取锁成功。");
                    return value;
                }
                //当 key 存在但没有设置剩余生存时间时
                if(jedis.ttl(key)==-1){
                    //设置失效时间
                    jedis.expire(key,timeout);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(null!=jedis){
                jedis.close();
            }
        }
        return null;
    }

参考文献:
为什么需要锁,锁分类,锁粒度

各种锁以及使用场景

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>