你手写过一把锁吗?你对轮询缓存怎么看?

当多个线程同时去操作一块内存的数据时如果不做一些限制,极其可能出现数据一致性问题。这时候,我们用一把锁锁住这块数据,持有钥匙者可以进入,不持有者等待钥匙用完再分配。所以在我看来啊,锁的本质就是一个标志位,代表当前线程是否有权限去操作目标内存,但是你的这把锁要穿透当前线程视野,穿透当前实例内存,穿透当前模块层级,到达整个系统可见共享的层次,且处理上要及时释放,再三过滤一切会出现死锁的情况。

所以常见的分布式锁,在可见性由redis缓存实现的解决方案里,通过大家都到redis这块实例上去拿钥匙,恰好进行同一代码块时 通常会将方法名以及时间戳带上某些id等按照一定规则作为key,value不要太大(大key可是会出现问题的,笔者生产环境就遇到过大key造成的数据流异常缓慢直接熔断请求)。为避免死锁也会保证在finally里强制释放锁。

实现lock接口的可重入锁与其使用demo

public class ReentrantTimeoutLock implements Lock {
    private static class Sync extends AbstractQueuedSynchronizer {
        private static final int FREE = 0;
        private static final int LOCKED = 1;
        private Thread owner = null;
        private int recursionCount = 0;
        @Override
        protected boolean tryAcquire(int arg) {
            Thread currentThread = Thread.currentThread();
            int state = getState();
            if (state == FREE) {
                if (compareAndSetState(FREE, LOCKED)) {
                    owner = currentThread;
                    recursionCount = 1;
                    return true;
                }
            } else if (currentThread == owner) {
                recursionCount++;
                return true;
            }
            return false;
        }
        @Override
        protected boolean tryRelease(int arg) {
            if (Thread.currentThread() != owner) {
                throw new IllegalMonitorStateException("Lock not owned by current thread");
            }
            recursionCount--;
            if (recursionCount == 0) {
                owner = null;
                setState(FREE);
            }
            return true;
        }
        @Override
        protected boolean isHeldExclusively() {
            return owner == Thread.currentThread();
        }
        Condition newCondition() {
            return new ConditionObject();
        }
    }
    private final Sync sync = new Sync();
    private final long timeout;
    public ReentrantTimeoutLock(long timeout) {
        this.timeout = timeout;
    }
    @Override
    public void lock() {
        sync.acquire(1);
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    @Override
    public boolean tryLock(long timeout,TimeUnit timeUnit) throws InterruptedException {
        return sync.tryAcquireNanos(1,timeUnit.toNanos(timeout));
    }
    @Override
    public void unlock() {
        sync.release(1);
    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

使用这把锁我们可以简单做一个接口上的ip限流与请求限流

@Component
public class AccessLimit {
    private final Lock lock = new ReentrantTimeoutLock(500); // 创建可重入锁对象
    private final HashMap<String, Long> ipAccesses = new HashMap<>(); // 存储IP地址访问时间
    private final HashMap<String, Long> apiAccesses = new HashMap<>(); // 存储接口访问时间

    /**
     * Limit access.
     *
     * @param ipAddress      the ip address
     * @param limitPerSecond the limit per second
     * @param apiName        the api name
     */
    public void limitAccess(String ipAddress, int limitPerSecond, String apiName) {
        try {
            lock.lock(); // 获取锁
            long currentTime = System.currentTimeMillis();
            Long lastIPAccess = ipAccesses.get(ipAddress);
            if (lastIPAccess != null && currentTime - lastIPAccess < 1000 / limitPerSecond) {
                throw new RuntimeException("IP refuse");
            }
            Long lastApiAccess = apiAccesses.get(apiName);
            if (lastApiAccess != null && currentTime - lastApiAccess < 1000 / limitPerSecond) {
                throw new RuntimeException("API refuse");
            }
            ipAccesses.put(ipAddress, currentTime);
            apiAccesses.put(apiName, currentTime);
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    /**
     * Release access.
     *
     * @param ipAddress the ip address
     * @param apiName   the api name
     */
    public void releaseAccess(String ipAddress, String apiName) {
        try {
            lock.lock(); // 获取锁
            ipAccesses.remove(ipAddress);
            apiAccesses.remove(apiName);
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

真实ip的获取在请求头的x-forwarded-for属性里

String ip = request.getHeader("x-forwarded-for");

对于如何获取真实ip的方法读者自行查找笔者以前的文章。

将限流组件使用在登录接口上如下形式

    @GetMapping("/login")
    @ResponseBody
    @ApiOperation(value = "登录", notes = "请求被限制3秒内单一ip无法连续访问,接口3秒内无法连续访问,无需携带token")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "managerPhone", value = "管理员电话", required = true, dataTypeClass = String.class),
            @ApiImplicitParam(name = "password", value = "密码", required = true, dataTypeClass = String.class),
            @ApiImplicitParam(name = "request", value = "请求对象", required = true, dataTypeClass = HttpServletRequest.class)})
    public GeneralResponse<Manager> managerLogin(String managerPhone, String password, HttpServletRequest request) {
        accessLimit.limitAccess(HttpFactory.getIpAddress(request), 3, "managerLogin");
        ResponseTwoArgInterface<String, String, GeneralResponse<Manager>> responseThreeArgInterface = (value1, value2) -> {
            Manager manager = managerService.managerLogin(value1, value2);
            if (Objects.nonNull(manager)) {
                jedisService.createToken(value1 + "&" + value2, MagicNumber.DEFAULT_OPTION_COUNT, MagicNumber.DEFAULT_TIME_OUT);
                return GeneralResponse.ServerSuccess(manager, "管理员登录成功");
            } else {
                return GeneralResponse.ServerError("管理员登录失败:请检查数据库在线状态或查看日志排查");
            }
        };
        return responseThreeArgInterface.returnResponse(managerPhone, password);
    }

经过测试可行。

使用redis设计一把分布式锁并设计轮询任务

@Component
public class RedisLock {
    private static final JedisPool jedisPool = JedisFactory.getJedisPool();
    private static final Jedis jedis = jedisPool.getResource();
    public Boolean getLock(String key, int expireTime) {
        try {
            jedis.auth(RedisConfig.AuthPassword);
            /*获取锁,如果上锁成功返回1*/
            Long lockIsSuccess = jedis.setnx(key, System.currentTimeMillis() + "");
            if (lockIsSuccess != null && lockIsSuccess == 1L) {
                /*锁计时,每一把锁都应设置一个计时释放的时间*/
                jedis.expire(key, expireTime);
                return true;
            } else {
                String lockGoneTime = jedis.get(key);
                if (lockGoneTime == null) {
                    lockIsSuccess = jedis.setnx(key, System.currentTimeMillis() + "");
                    if (lockIsSuccess != null && lockIsSuccess == 1L) {
                        jedis.expire(key, expireTime);
                    }
                    return lockIsSuccess != null && lockIsSuccess == 1L;
                } else {
                    long currentTimeMillis = System.currentTimeMillis();
                    if (currentTimeMillis - Long.parseLong(lockGoneTime) < expireTime * 1000L) {
                        return false;
                    } else {
                        String lockNowTime = jedis.getSet(key, currentTimeMillis + "");
                        if (lockNowTime == null || lockNowTime.equals(lockGoneTime)) {
                            jedis.expire(key, expireTime);
                        }
                        return lockNowTime == null || lockNowTime.equals(lockGoneTime);
                    }
                }
            }
        }finally {
            jedis.close();
        }
    }

    public void unLock(String key){
        try {
            jedis.auth(RedisConfig.AuthPassword);
            jedis.expire(key, 0);
        }finally {
            jedis.close();
        }
    }
}

这把锁也是可重入锁,这里的key读者可以自行设计,需要注意的是redis连接池设置,以及客户端连接后要及时释放资源;

将这把锁运用到轮询任务组件中的代码如下:

@Component
@DependsOn(value = {"ThreadPool"})
@Slf4j
public class RoundCheckTask {
    @Resource
    private ThreadPoolTaskExecutor threadPool;
    @Resource
    private RedisLock redisLock;

    private static final JedisPool jedisPool = JedisFactory.getJedisPool();
    private static final Jedis jedis = jedisPool.getResource();

    public void roundCheckCache(String key, String taskId, int roundTime) {
        jedis.auth(RedisConfig.AuthPassword);
        /*尝试获取一把轮询任务的锁,key由时间戳组成保证并发抢锁*/
        Boolean lock = redisLock.getLock(key, roundTime);
        if (lock) {
            try {
                long start = System.currentTimeMillis();
                threadPool.execute(() -> {
                    while (true) {
                        Long ttl = jedis.ttl(taskId);
                        if (ttl == null || ttl == 0L) {
                            runTaskThread(taskId, roundTime);
                            break;
                        }
                        if (System.currentTimeMillis() - start > roundTime && ttl > roundTime) {
                            /*循环进入的时间大于轮询时长直接退出轮询*/
                            break;
                        }
                        try {
                            Thread.sleep(500);
                        } catch (Exception e) {
                            throw new RuntimeException("thread sleep exception");
                        }
                    }
                }, MagicNumber.DEFAULT_TIME_OUT);
            } catch (Exception e) {
                throw new RuntimeException("start round check thread is fail");
            } finally {
                redisLock.unLock(key);
                jedis.close();
            }
        } else {
            jedis.close();
        }
    }

    public void runTaskThread(String taskId, int runTime) {
        jedis.auth(RedisConfig.AuthPassword);
        Boolean lock = redisLock.getLock(taskId, runTime);
        if (lock) {
            try {
                CountDownLatch countDownLatch = new CountDownLatch(1);
                threadPool.execute(() -> {
                    /*执行业务逻辑*/
                    System.out.println(taskId);
                    countDownLatch.countDown();
                });
                countDownLatch.await();
            } catch (Exception e) {
                throw new RuntimeException("task service running error");
            } finally {
                redisLock.unLock(taskId);
                jedis.close();
            }
        } else {
            jedis.close();
        }
    }

}

这里轮询的逻辑也就不仔细讲解了,核心的地方就在

                        Long ttl = jedis.ttl(taskId);
                        if (ttl == null || ttl == 0L) {
                            runTaskThread(taskId, roundTime);
                            break;
                        }
                        if (System.currentTimeMillis() - start > roundTime && ttl > roundTime) {
                            /*循环进入的时间大于轮询时长直接退出轮询*/
                            break;
                        }
                        try {
                            Thread.sleep(500);
                        } catch (Exception e) {
                            throw new RuntimeException("thread sleep exception");
                        }

此处使用计数器来等待业务逻辑的执行。

对于redis锁的实现方案,redisson是不错的选型,有兴趣的读者可以了解了解redisson的redlock

最后笔者祝各位劳动节快乐~ 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ForestSpringH

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

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

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

打赏作者

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

抵扣说明:

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

余额充值