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

前言

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

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> 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

更多精彩内容

关注我们

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容