高性能MySQL

3、分布式锁

2019-05-06  本文已影响2人  小manong

对于分布式锁可以从几个问题入手:

  1. 分布式锁应用的场景是什么样的呢?
  2. 分布式锁的实现是怎么样的呢?
  3. 分布式锁的实现方案应用有什么优劣势?
  4. 分布式锁的方案如何选择?等

一、分布式锁概念

1、什么是分布式锁
2、分布式锁的设计目标

1、可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
2、这把锁要是一把可重入锁(避免死锁)
3、这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
4、这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
5、有高可用的获取锁和释放锁功能(服务高可用,系统稳健
6、获取锁和释放锁的性能要好
7、锁的自动续约与自动释放
8、代码高度抽象,业务接入非常简单
9、可视化的管理后台,监控与管理

3、常见分布式锁解决方案

MySql
Zk
Redis
自研分布式锁:如谷歌的Chubby。
etcd

二、基于mysql实现分布式锁
1、基于表主键唯一做分布式锁

原理:利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。(联合主键或者唯一主键但是不能使自增主键)

1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
5、这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。

1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
5、非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁

2、基于数据库排他锁做分布式锁

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
思路:我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,通过connection.commit()操作来释放锁。
示例:

BEGIN;(确保以下2步骤在一个事务中:)
SELECT * FROM tb_product_stock WHERE product_id=1 FOR UPDATE--->product_id有索引,锁行,无索引,表锁(加锁阶段)
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1--->更新库存
COMMIT;( 解锁阶段)

性能依赖于mysql,同时虽然使用了索引,在查询优化的时候未必使用行锁有可能使用了表锁。也有可能引发死锁等其他意外发生

3、基于版本控制实现的乐观锁

BEGIN;(确保以下2步骤在一个事务中:)
SELECT number FROM tb_product_stock WHERE product_id=1--》查询库存总数,不加锁
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1 AND number=第一步查询到的库存数--》number字段作为版本控制字段(这里版本的控制可以根据业务调整)
COMMIT;

4、mysql作为分布式锁总结
三、redis作为分布式锁
1、基于jedis实现分布式锁
/**
 * 基于jedis实现的分布式锁
 */
public class RedisDistributeLockUtils {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * @param jedis
     * @param lockKey
     * @param requestId   通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。
     *                    requestId可以使用UUID.randomUUID().toString()方法生成。也就是锁谁加的锁谁来释放
     * @param expiredTime
     * @return
     */
    public boolean getDistributeLock(Jedis jedis, String lockKey, String requestId, Long expiredTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expiredTime);
        if (LOCK_SUCCESS.equalsIgnoreCase(result)) {
            return true;
        }
        return false;
    }

    public boolean releaseDistributeLock(Jedis jedis, String lockKey, String requestId) {
        /**
         * 1、过程:将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,
         * ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
         * 2、原理:首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。
         * 那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。
         */
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
2、redlock算法实现分布式锁

1、获取当前时间(单位是毫秒)。
2、轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。

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

(2)、配置文件

spring.redisson.password=123456
spring.redisson.clusters=127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372

(3)配置redisson

@Configuration
public class RedissonConfig {
    @Value("${spring.redisson.clusters}")
    private  String cluster;
    @Value("${spring.redisson.password}")
    private String password;
    
    @Bean
    public RedissonClient getRedisson(){
        String[] nodes = cluster.split(",");
        //redisson版本是3.5,集群的ip前面要加上“redis://”,不然会报错,3.2版本可不加
        for(int i=0;i<nodes.length;i++){
            nodes[i] = "redis://"+nodes[i];
        }
        RedissonClient redisson = null;
        Config config = new Config();
        config.useClusterServers() //这是用的集群server
        .setScanInterval(2000) //设置集群状态扫描时间 
        .addNodeAddress(nodes)
        .setPassword(password);
        redisson = Redisson.create(config);
        return redisson;
    }
}

(4)分布式锁使用

@Service
public class DistributeLockServiceImpl{
    @Autowired
    private RedissonClient redissonClient;

private final static String LOCK_KEYP_PREFIX="redisson:lock:test:";
    @overiide
    public void redissonTest(Long userId) {
       //获取key
        String redisLockKey=LOCK_KEYP_PREFIX.concat(String.ValueOf(userId));
       //获取redisson锁
        RLock rlock = redissonClient.getLock(redisLockKey);
  //设置锁超时时间,防止异常造成死锁
        rlock.lock(20, TimeUnit.SECONDS);
        try{
           //todo 执行业务逻辑
        } catch(Exception e){
            //异常处理
        }finally{
            //释放锁
            rlock.unlock();
        }  
    }
}
5、redLock算法实现的分布式锁总结

三、zookeeper实现的分布式锁

四、基于etcd实现的分布式锁

五、几种主流的分布锁对比


参考:
http://www.importnew.com/27477.html
https://juejin.im/post/59f592c65188255f5c5142d2
https://zhuanlan.zhihu.com/p/42056183
https://juejin.im/post/5bbb0d8df265da0abd3533a5
http://www.spring4all.com/question/158

上一篇下一篇

猜你喜欢

热点阅读