架构实战篇(十八):Spring Boot Redis实现分布式锁

bigdata 2018-11-02 17:09:29 ⋅ 1002 阅读

前言

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。

Redis 的优点

  • Redis有很高的性能

  • Redis命令对此支持较好,实现起来比较方便

这也是选用Redis 做分布式锁的原因
启动 mysql 和 redis

一、先看下目录结构

二、一般的业务代码

@Override
    public Result loginByWx(String openId) {
        User user = userMapper.findByOpenId(openId);
        if (user != null) {
            return Result.of(0"OK", user.getUserId());
        }

        // 模拟业务执行时间
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.info("create user by openId: {}", openId);
        user = new User();
        user.setOpenId(openId);
        userMapper.save(user);

        return Result.of(0"OK", user.getUserId());
    }

如果在并发或者短时间内重复请求会出现重复创建用户的问题

三、了解Redis set方法

Jedis.set 方法

public String set(final String key, final String value, final String nxxx, final String expx, final int time)

存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。
key 锁的名字
value 锁的内容
nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
time 过期时间,单位是expx所代表的单位。

四、编写分布式锁

package com.itunion.demo.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;

@Service
public class RedisLockServiceImpl implements LockService {

    private static Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
    private static final String LOCK_SUCCESS = "OK";

    @Autowired
    protected RedisTemplate<String, String> redisTemplate;

    @Override
    public void unLock(String key) {
        redisTemplate.delete(key);
    }

    public synchronized boolean isLock(String key, int seconds) {
        return redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                Jedis conn = (Jedis) connection.getNativeConnection();
                String result = conn.set(key, "1""NX""EX", seconds);
                return result != null && result.equalsIgnoreCase(LOCK_SUCCESS);
            }
        });
    }

    @Override
    public <T> lockExecute(String key, LockExecute<T> lockExecute) {
        boolean isLock = isLock(key, 15);
        final int SLEEP_TIME = 200;
        final int RETRY_NUM = 20;
        int i;
        for (i = 0; i < RETRY_NUM; i++) {
            if (isLock) {
                break;
            }
            try {
                log.debug("wait redis lock key > {}", key);
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException e) {
                log.warn("wait redis error {}", e.getMessage());
            }
            isLock = isLock(key, 150);
        }
        if (!isLock) {
            log.warn("wait lock time out key > {}", key);
            return lockExecute.waitTimeOut();
        }
        try {
            if (i > 0) log.debug("wait lock retry count {}", i);
            return lockExecute.execute();
        } finally {
            unLock(key);
        }
    }
}

五、在业务上添加锁

public Result loginByWxLock(String openId){
        // 更新点赞数量
        String lockKey = "lock:loginByWx:" + openId;

        return lockService.lockExecute(lockKey, new LockService.LockExecute<Result>() {
            @Override
            public Result execute() {
                return loginByWx(openId);
            }

            @Override
            public Result waitTimeOut() {
                return Result.of(-1"访问太频繁");
            }
        });
    }

六、单元测试

@Test
    public void contextLoads() throws InterruptedException {
        int size = 100;
        CountDownLatch countDownLatch = new CountDownLatch(size);
        List<Result> results = new ArrayList<>();

        for (int i = 0; i < size; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Random random = new Random();
                    String openId = "openId:" + random.nextInt(10);
                    results.add(userService.loginByWxLock(openId));
                    countDownLatch.countDown();
                }
            }).start();
        }

        countDownLatch.await();

        // 统计信息
        int loginSuccessNum = 0;
        int loginFailedNum = 0;
        for (Result result : results) {
            if (result.getCode() == 0) {
                loginSuccessNum++;
            }else{
                loginFailedNum++;
            }
        }
        System.out.println("登录成功总数:" + loginSuccessNum);
        System.out.println("登录失败总数:" + loginFailedNum);
    }

测试结果

Github: https://github.com/qiaohhgz/spring-boot-redis-lock.git

关注我们



全部评论: 0

    我有话说:

    架构实战二):Spring Boot 分布式Session共享Redis

    分布式Web网站一般都会碰到集群session共享问题,小编整理了一套解决方案,内附GitHub 源码地址哦~~~

    架构实战七):Spring Boot Assembly 整合 thymeleaf

    如何让服务器上的 sprig boot 项目升级变的方便快捷

    架构实战):Spring Boot 集成 Dubbo

    Dubbo是阿里巴巴SOA服务化治理方案的核心框架,一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案。

    架构实战一):Spring Boot 集成企业级搜索引擎 SolrCloud

    Solr是以Lucene为基础实现的文本检索应用服务。Solr部署方式有单机方式、多机Master-Slaver方式、Cloud方式。

    架构实战五):Spring Boot 解耦之事件驱动

    通过使用spring 事件来解决业务代码的耦合

    架构实战(七):Spring Boot Data JPA 快速入门

    Spring Data JPA 是Spring Data 的一个子项目,它通过提供基于JPA的Repository极大了减少了操作JPA的代码。

    架构实战(六):Spring Boot RestTemplate的使用

    RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。

    架构实战(三)-Spring Boot架构搭建RESTful API案例

    之前分享了Spring Boot 整合Swagger 让API可视化和前后端分离架构 受到了大家一致好评 ,本节就接着上节的代码做了详细的查询代码的补充和完善并搭建RESTful API架构案例。

    架构实战四):Spring Boot 多缓存实战

    多场景下的不同缓存策略解决方案

    架构实战三):Spring Boot Logback 邮件通知

    日志对于应用程序来说是非常重要的,当你的程序报错了,而你又不知道是多么可怕的一件事情,本文使用logback把程序报错信息邮件到开发者

    架构实战(一)-Spring Boot+MyBatis基础架构搭建

    Spring的追求一定是简单点简单点,让java的开发变得更加简单、容易。瞧瞧的告诉你们直接copy就能用哦~~~

    码云推荐:一个优秀的分布式spring boot/Spring Cloud API限流框架,特别适合微服务架构

    一个优秀的分布式spring boot/Spring Cloud API限流框架,特别适合微服务架构.

    微服务架构实战(六):Spring boot2.x 集成阿里大鱼短信接口详解与Demo

    Spring boot2.x 集成阿里大鱼短信接口,发送短信验证码及短信接口详解。

    架构实战):Spring Boot 集成 Druid 数据源监控

    Druid是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。

    架构实战六):Spring Boot Assembly服务化打包

    使用assembly来打包springboot微服务项目,让发布更简单

    Martian框架发布 3.0.3 版本,Redis分布式

    项目简介 Martian 是一个声明式 API 编程(DAP)框架,可以帮助你快速开发后端服务。 以HttpServer作为 http服务,彻底脱离Tomcat这一类的Web容器和Servlet,同时也让项目减少了几个依赖 声明式API,让Co...

    Redis系列四

      本文目标 1. 熟悉乐观ABA概念 2. 理解掌握redis事务以及watch回滚; 3. 实战redis 乐观 乐观是一种不会阻塞其他线程并发的机制,它不会使用数据库的

    架构实战(九):Spring Boot 集成 RocketMQ

    快速集成阿里开源消息队列 RocketMQ