• 基于Redis手工实现分布式锁


    1.分布式锁概述

    1.1什么是分布式锁

            随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景,与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。而在分布式系统中访问共享资源就需要一种互斥机制,来解决分布式系统中控制共享资源访问的问题,以保证数据一致性,在这种情况下,我们就需要用到分布式锁。

    总结:

            1)应用场景:分布式系统。

            2)作用:提供一种共享资源访问的互斥机制,保证数据一致性。

    1.2分布式锁的特性

    • 互斥“”在分布式系统环境下,一个方法在同一时间只能被一个线程执行(即获取锁)
    • 高可用:高可用的获取锁与释放锁
    • 高性能:高性能的获取锁与释放锁
    • 可重入:具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
    • 防止死锁:具备锁失效机制,超时即自动解锁,
    • 非阻塞:即如果线程没有获取到锁将,直接返回获取锁失败,而不会一直阻塞

    2.分布式锁的实现

            目前分布式锁常见的三种实现方式:

            1、基于数据库实现分布式锁;

            2、基于缓存(Redis等)实现分布式锁;

            3、基于Zookeeper实现分布式锁。

            本文就基于Redis手工实现分布式锁,当前现在主流的是基于Redisson工具包实现(不在本文范围内),但手工实现原理基本一致。

    Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。

    2.1 锁接口定义

            接口比较简单,主要定义了加锁和释放锁两个方法

    1. package com.example.demo.redis.lock;
    2. import java.util.concurrent.TimeUnit;
    3. /**
    4. *
    5. * @ClassName: RedisDistributeLock
    6. * @Description: Redis分布式锁
    7. * @Author: liulianglin
    8. * @DateTime 2022830日 下午5:47:47
    9. */
    10. public interface RedisDistributeLock {
    11. /**
    12. *
    13. * @Description: 加锁
    14. * @Author: liulianglin
    15. * @Datetime: 2022830日 下午5:49:43
    16. * @param key 主键
    17. * @param timeout 超时时间
    18. * @param unit 超时时间单位
    19. * @return boolean true:加锁成功;false:加锁失败
    20. * @throws
    21. */
    22. boolean tryLock(String key, long timeout, TimeUnit unit);
    23. /**
    24. *
    25. * @Description: 释放锁。加锁和释放锁的线程必须保证是同一个。
    26. * @Author: liulianglin
    27. * @Datetime: 2022830日 下午5:51:20
    28. * @param key 主键
    29. * @throws
    30. */
    31. void releaseLock(String key);
    32. }

     

    2.2 接口实现

             实现RedisDistributeLock接口,其中针对分布式锁的一些特性进行实现,如加锁解锁线程一致性保证、可重入、非阻塞等(参考注释)

    1. package com.example.demo.redis.lock;
    2. import java.util.Objects;
    3. import java.util.UUID;
    4. import java.util.concurrent.TimeUnit;
    5. import org.springframework.beans.factory.annotation.Autowired;
    6. import org.springframework.data.redis.core.StringRedisTemplate;
    7. public class RedisDistributeLockImpl implements RedisDistributeLock{
    8. @Autowired
    9. private StringRedisTemplate stringRedisTemplate;
    10. private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    11. // 计数器
    12. private ThreadLocal<Integer> counterThreadLocal = new ThreadLocal<Integer>();
    13. @Override
    14. public boolean tryLock(String key, long timeout, TimeUnit unit) {
    15. Boolean isLocked = false;
    16. if (Objects.isNull(threadLocal.get())) {
    17. String uuid = UUID.randomUUID().toString();
    18. threadLocal.set(uuid);
    19. isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
    20. // 如果获取锁失败,通过自旋尝试获取锁,
    21. if (!isLocked) {
    22. for (;;) {
    23. isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
    24. if (isLocked) {
    25. // 获取成功立即退出
    26. break;
    27. }
    28. }
    29. }
    30. /*
    31. * 启动一个线程扮演“看门狗”的角色,不断更新锁索过期时间
    32. *
    33. * 注意:这里将stringRedisTemplate对象传给看门狗
    34. */
    35. new Thread(new WatchDogThread(uuid, stringRedisTemplate, key)).start();
    36. } else {
    37. isLocked = true;
    38. }
    39. // 加锁成功后
    40. if (isLocked) {
    41. Integer curCount = counterThreadLocal.get() == null ? 0 : counterThreadLocal.get();
    42. counterThreadLocal.set(curCount++);
    43. }
    44. return isLocked;
    45. }
    46. @Override
    47. public void releaseLock(String key) {
    48. // 保证解锁和加锁线程是同一个,防止避免了一个线程对程序进行了加锁操作后,其他线程对这个锁进行了解锁操作的问题
    49. String uuid = stringRedisTemplate.opsForValue().get(key);
    50. if (!Objects.isNull(threadLocal.get()) &&
    51. threadLocal.get().equals(uuid)) {
    52. Integer curCount = counterThreadLocal.get();
    53. if (Objects.isNull(curCount) || (--curCount)<=0) {
    54. stringRedisTemplate.delete(key);
    55. // 获取对应的看门狗线程的ID
    56. String watchDogThreadIdStr = stringRedisTemplate.opsForValue().get(uuid);
    57. // 获取看门狗
    58. Thread watchDogThread = ThreadUtils.getThreadByThreadId(Long.valueOf(watchDogThreadIdStr));
    59. if (!Objects.isNull(watchDogThread)) {
    60. // 终端看门狗
    61. watchDogThread.interrupt();
    62. stringRedisTemplate.delete(uuid);
    63. }
    64. }
    65. }
    66. }
    67. }

     

    2.3 看门狗线程

            看门狗线程,更新锁的超时时间,保证锁的释放一定是在加锁线程业务代码执行完毕之后,
    防止在业务处理过程中锁超时失效,其他线程依旧能够获取到锁。

    1. package com.example.demo.redis.lock;
    2. import java.util.concurrent.TimeUnit;
    3. import org.springframework.data.redis.core.StringRedisTemplate;
    4. /**
    5. *
    6. * @ClassName: WatchDogThread
    7. * @Description: 看门狗线程,更新锁的超时时间,保证锁的释放一定是在加锁线程业务代码执行完毕之后,
    8. * 防止在业务处理过程中锁超时失效,其他线程依旧能够获取到锁。
    9. * @Author: liulianglin
    10. * @DateTime 2022831日 上午9:33:07
    11. */
    12. public class WatchDogThread implements Runnable{
    13. private String uuid;
    14. private String key;
    15. private StringRedisTemplate stringRedisTemplate;
    16. public WatchDogThread(String uuid, StringRedisTemplate stringRedisTemplate, String key) {
    17. this.uuid = uuid;
    18. this.stringRedisTemplate = stringRedisTemplate;
    19. this.key = key;
    20. }
    21. @Override
    22. public void run() {
    23. /*
    24. * 以uuid为key,将当前线程的ID作为value保存到Redis中
    25. 在RedisDistributeLockImpl删除锁时需要通过uuid获取到当前线程ID,然后停止当前看门狗线程。
    26. */
    27. stringRedisTemplate.opsForValue().set(uuid, String.valueOf(Thread.currentThread().getId()));
    28. // 循环更新所的过期时间
    29. while(true) {
    30. stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS);
    31. try {
    32. // 每秒执行1
    33. TimeUnit.SECONDS.sleep(1);
    34. }catch(InterruptedException e) {
    35. e.printStackTrace();
    36. }
    37. }
    38. }
    39. }

    2.4 线程操作工具类

            主要是通过线程ID获取线程,目前只有一个方法,比较简单

    1. package com.example.demo.redis.lock;
    2. public class ThreadUtils {
    3. /**
    4. *
    5. * @Description: 通过ThreadID获取线程对象
    6. * @Author: liulianglin
    7. * @Datetime: 2022831日 上午10:00:54
    8. * @param threadId
    9. * @return Thread
    10. * @throws
    11. */
    12. public static Thread getThreadByThreadId(long threadId) {
    13. ThreadGroup group = Thread.currentThread().getThreadGroup();
    14. while(group != null) {
    15. Thread[] threads = new Thread[(int)(group.activeCount() * 1.2)];
    16. int count = group.enumerate(threads, true);
    17. for(int i = 0; i < count; i++) {
    18. if(threadId == threads[i].getId()) {
    19. return threads[i];
    20. }
    21. }
    22. group = group.getParent();
    23. }
    24. return null;
    25. }
    26. }

     

     完毕。。。。

  • 相关阅读:
    ERP系统排行
    海藻酸钠-聚乙二醇-反式环辛烯|TCO-PEG-alginate|海藻酸钠-聚乙二醇-PEG-TCO
    代码随想录39——动态规划:62不同路径、63不同路径II
    springboot+vue3+elementui plus汽车租赁网站源码
    Rokid AR Lite空间计算套装发布,中国空间计算踏上差异化领先之路
    CSS元素
    maven安装配置
    Android 启动优化系列 —— 系统启动流程
    基于TCP Socket和Websocket实现的相互即时通信系统
    【SSM框架】Mybatis详解06 对象分析、注册别名、设置日志输出
  • 原文地址:https://blog.csdn.net/liulianglin/article/details/126620877