Redission分布式锁

场景介绍:一个支付订单的微服务根据订单状态查询订单,查询到一个订单状态为1(待支付),然后用户未完成支付,一个取消超时订单的微服务此时根据订单状态也查询到这个订单,随着时间的推移,用户在某个时刻完成了订单支付,并将订单状态修改为2(已支付),过了超时时间,取消超时订单的微服务将这个订单的状态修改为5(取消订单),这个时候,就出现了订单状态不一致的情况,这个时候就需要使用分布式锁来解决这个问题。

可以使用redis或者zookeeper来实现分布式锁,redis相对于可用性强,效率快,zookeeper相对于保持数据一致性强,在redis集群中,如果分布式锁是在master上的,如果master在宕机之前没有同步锁,就有可能出现没有分布式锁的问题,redis是单线程的执行命令,sexnx  name zhangsan,如果name存在就不会创建name,并返回0,如果不存在创建name并返回1,那么可以通过谁创建的name,代表谁拿到这个分布式锁,创建name的微服务拿到锁,执行完业务之后,先判断这个key的锁是不是自己加的,如果是,就删除这个key,这是基本思路。在实际生产情况下,拿到锁的微服务如果宕机了,锁没有释放,怎么办,可以给key设置过期时间;如果拿到锁的微服务在拿到锁的基础上又加了锁,就要使用可重入锁;锁释放之后,要保证实现请求的先后顺序拿到锁,要实现公平锁;锁没有释放,一个请求去拿锁,如果没有拿到,可以使用自旋,就是每隔一段时间,访问一下redis,判断锁有没有释放,如果不想自旋,要设置时间,过了时间,没拿到锁,就做别的事去;综上所述,如果自己实现分布式锁要考虑很多问题,要么有框架已经实现了分布式锁,我们直接去用就可以了。Redission就是一个实现了分布式锁的框架,基于redis来实现分布式锁,上面说的几个方面,它都有实现,超时机制、自动续约机制,阻塞加锁和非阻塞加锁(去加锁发现锁已经加了,不一直等待锁的释放)、可重入锁、公平锁、非公平锁等

Map:Map存储在redis中,多个微服务可以操作同一个Map。还有List等

分布式锁的工作原理

它加锁用的是redis中的hash类型,hset 大key 小key value,大key是锁,小key是客户端ID,value是加锁次数(实现可重入锁的功能), 那加锁的时候,要去判断有没有这个锁等,去判断的过程中,如果redis中的数据被修改了怎么办,这就要加锁再去判断,它把这多个命令放在一个lua脚本当中,这个lua脚本可以保证里面的所有命令执行是原子性的,不是这个脚本里面的命令,要么在这个脚本执行之前或者之后执行;如果拿到锁的服务宕机了,锁没有释放怎么办,redis中锁的默认失效时间是30秒,如果30秒之后,拿到锁的微服务的业务代码还没有执行完,redission有一个自动续约功能,通过watchdog机制来实现,只要微服务加锁成功了,它会启动后台的一个调度线程,这个线程会每隔10秒把失效时间重置为30秒,如果加锁的redis节点宕机了,那么watchdog也会释放,那么30秒之后,这个锁也会释放。redission适配redis的各种架构的集群,解锁也是用lua脚本来实现,因为解锁也需要多个命令,要保证这多个命令的原子性。当value减为零的时候,就把redis中这个key删除。

Redission中提供了一个RLock接口,这个接口实现了jdk中的Lock接口

/**
 * Copyright (c) 2013-2019 Nikita Koksharov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.redisson.api;

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

/**
 * Distributed implementation of {@link java.util.concurrent.locks.Lock}
 * Implements reentrant lock.
 * Use {@link RLock#getHoldCount()} to get a holds count.
 *
 * @author Nikita Koksharov
 *
 */

public interface RLock extends Lock, RLockAsync {

    /**
     * Returns name of object
     *
     * @return name - name of object
     */
    String getName();
    
    /**
     * Acquires the lock.
     *
     * <p>If the lock is not available then the current thread becomes
     * disabled for thread scheduling purposes and lies dormant until the
     * lock has been acquired.
     *
     * If the lock is acquired, it is held until <code>unlock</code> is invoked,
     * or until leaseTime have passed
     * since the lock was granted - whichever comes first.
     *
     * @param leaseTime the maximum time to hold the lock after granting it,
     *        before automatically releasing it if it hasn't already been released by invoking <code>unlock</code>.
     *        If leaseTime is -1, hold the lock until explicitly unlocked.
     * @param unit the time unit of the {@code leaseTime} argument
     * @throws InterruptedException - if the thread is interrupted before or during this method.
     */
    void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException;

    /**
     * Returns <code>true</code> as soon as the lock is acquired.
     * If the lock is currently held by another thread in this or any
     * other process in the distributed system this method keeps trying
     * to acquire the lock for up to <code>waitTime</code> before
     * giving up and returning <code>false</code>. If the lock is acquired,
     * it is held until <code>unlock</code> is invoked, or until <code>leaseTime</code>
     * have passed since the lock was granted - whichever comes first.
     *
     * @param waitTime the maximum time to aquire the lock
     * @param leaseTime lease time
     * @param unit time unit
     * @return <code>true</code> if lock has been successfully acquired
     * @throws InterruptedException - if the thread is interrupted before or during this method.
     */
//如果加上锁,返回true,否则返回false,非阻塞加锁,可以自己设置锁的失效时间,如果自己设置了失效时间,watchdog将会失效
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

    /**
     * Acquires the lock.
     *
     * <p>If the lock is not available then the current thread becomes
     * disabled for thread scheduling purposes and lies dormant until the
     * lock has been acquired.
     *
     * If the lock is acquired, it is held until <code>unlock</code> is invoked,
     * or until leaseTime milliseconds have passed
     * since the lock was granted - whichever comes first.
     *
     * @param leaseTime the maximum time to hold the lock after granting it,
     *        before automatically releasing it if it hasn't already been released by invoking <code>unlock</code>.
     *        If leaseTime is -1, hold the lock until explicitly unlocked.
     * @param unit the time unit of the {@code leaseTime} argument
     *
     */
//如果没有加上锁,会自旋
    void lock(long leaseTime, TimeUnit unit);

    /**
     * Unlocks lock independently of state
     *
     * @return <code>true</code> if lock existed and now unlocked otherwise <code>false</code>
     */
    boolean forceUnlock();

    /**
     * Checks if this lock locked by any thread
     *
     * @return <code>true</code> if locked otherwise <code>false</code>
     */
    boolean isLocked();

    /**
     * Checks if this lock is held by the current thread
     *
     * @param threadId Thread ID of locking thread
     * @return <code>true</code> if held by given thread
     * otherwise <code>false</code>
     */
    boolean isHeldByThread(long threadId);

    /**
     * Checks if this lock is held by the current thread
     *
     * @return <code>true</code> if held by current thread
     * otherwise <code>false</code>
     */
    boolean isHeldByCurrentThread();

    /**
     * Number of holds on this lock by the current thread
     *
     * @return holds or <code>0</code> if this lock is not held by current thread
     */
    int getHoldCount();

    /**
     * Remaining time to live of this lock 
     *
     * @return time in milliseconds
     *          -2 if the lock does not exist.
     *          -1 if the lock exists but has no associated expire.
     */
    long remainTimeToLive();
    
}

redission依赖

 <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.11.2</version>
        </dependency>

 

package com.itheima.itheimadistributelock.core.redis;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;


/**
 * @author Administrator
 */
public class RedssionLock {
	//zookeeper 分布锁
	public static RLock getLock() {

		Config config = new Config();
		//指定使用集群部署方式
		//config.useClusterServers();
		//指定使用单节点部署方式
		config.useSingleServer().setAddress("redis://localhost:6379");//.setPassword("mkxiaoer");
//		config.useSingleServer().setPassword("root");
		config.useSingleServer().setConnectionPoolSize(500);//设置对于master节点的连接池中连接数最大为500
		config.useSingleServer().setIdleConnectionTimeout(10000);//如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
		config.useSingleServer().setConnectTimeout(30000);//同任何节点建立连接时的等待超时。时间单位是毫秒。
		config.useSingleServer().setTimeout(3000);//等待节点回复命令的时间。该时间从命令发送成功时开始计时。
		config.useSingleServer().setPingTimeout(30000);
		//获取RedissonClient对象
		RedissonClient redisson = Redisson.create(config);
		//获取锁对象
		RLock rLock = redisson.getLock("lock.lock");
		//获取公平锁
		//rLock = redisson.getFairLock("lock.lock");
		return rLock;
	}

	/**
	 * 演示可重入锁
	 * @param args
	 */
	public static void main(String[] args) {
		RLock lock = RedssionLock.getLock();
		lock.lock();
		System.out.println("获取锁成功-----1次");
		lock.lock();
		System.out.println("获取锁成功-----2次");
		try {
			Thread.sleep(100000);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		// 整个方法解锁
		lock.unlock();
	}
}

可以看到TTL的变化,有自动续约功能。

案例测试

package com.itheima.itheimadistributelock;

import com.itheima.itheimadistributelock.config.SellConstants;
import com.itheima.itheimadistributelock.service.GoodsService;
import com.itheima.itheimadistributelock.service.impl.GoodsRedissionLockSerivice;
import com.itheima.itheimadistributelock.service.impl.GoodsServiceImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ItheimaDistributelockApplicationTests implements ApplicationContextAware {

    private ApplicationContext applicationContext;
    long timed = 0;
    @Before
    public void start(){
        timed = System.currentTimeMillis();
        System.out.println("开始测试....");
    }
    @After
    public void end(){
        System.out.println("结束测试,执行时长是:" + (System.currentTimeMillis() - timed) / 1000 );
        System.out.println("共售出:"+ SellConstants.sellNum.get() + "台产品");
    }
    @Test
    public void buy(){
        //模拟请求数量
        int serviceNum =1;//4台tomcat 107 班 100
        int requesetSize = 100;//每台服务多少并发进入到系统
        //倒计数器。用于模拟高并发 juc CountDownLatch 主线分布式锁,线程的阻塞和唤醒jdk5 juc编程提供并发编程类
        CountDownLatch countDownLatch = new CountDownLatch(1);
        //循环创建N个线程
        List<Thread> threads = new ArrayList<>();
        String userId = "100",goodsId = "apple";
        int stock = 2;
        //模拟服务器的数量
        for (int i = 0; i < serviceNum; i++) {
            // 未使用锁
            GoodsService goodsService = applicationContext.getBean(GoodsServiceImpl.class);
            // 使用 synchronized 锁
//            GoodsService goodsService = applicationContext.getBean(GoodsSyncSerivice.class);
            // 使用 zookeeper 实现分布式锁
//            GoodsService goodsService = applicationContext.getBean(GoodsZkLockSerivice.class);
            // 使用 Jedis ==> Redis 实现分布式锁
//            GoodsService goodsService = applicationContext.getBean(GoodsJedisLockService.class);
            // 使用 Redission ==> Redis 实现分布式锁
            //GoodsService goodsService = applicationContext.getBean(GoodsRedissionLockSerivice.class);
            //模拟每台服务器发起请求的数量
            for (int i1 = 0; i1 < requesetSize; i1++) {
                Thread thread = new Thread(()->{
                    try {
                        //等待countdownlatch值为0,也就是其他线程就绪后,在运行后续的代码。
                        countDownLatch.await();
                        //执行吃饭的动作
                        goodsService.buy(userId,goodsId,stock);
                    }catch (Exception ex){
                        throw new RuntimeException(ex);
                    }
                });
                //添加线程到集合中
                threads.add(thread);
                //启动线程
                thread.start();
            }
        }
        //并发执行所有请求
        countDownLatch.countDown();
        threads.forEach((e)->{
            try {
                e.join();
            }catch (Exception ex){
               throw new RuntimeException(ex);
            }
        });
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

 数据库中apple的数量

运行完代码之后查看数据库 

 

注释掉

// 未使用锁
GoodsService goodsService = applicationContext.getBean(GoodsServiceImpl.class);

 

打开

// 使用 Redission ==> Redis 实现分布式锁
GoodsService goodsService = applicationContext.getBean(GoodsRedissionLockSerivice.class);

 

恢复数据库中apple的数量为100

运行完代码查看数据库

 

说明项目有bug,有时候正常,有时候不正常

 

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

敲代码的翠花

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值