架構(gòu)實(shí)戰(zhàn)篇(十八):Spring Boot Redis實(shí)現(xiàn)分布式鎖

前言

目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題。
在很多場景中,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來支持,比如分布式事務(wù)、分布式鎖等。

Redis 的優(yōu)點(diǎn)

  • Redis有很高的性能
  • Redis命令對此支持較好,實(shí)現(xiàn)起來比較方便

這也是選用Redis 做分布式鎖的原因
啟動 mysql 和 redis

一、先看下目錄結(jié)構(gòu)

二、一般的業(yè)務(wù)代碼

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

        // 模擬業(yè)務(wù)執(zhí)行時間
        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());
    }

如果在并發(fā)或者短時間內(nèi)重復(fù)請求會出現(xiàn)重復(fù)創(chuàng)建用戶的問題

三、了解Redis set方法

Jedis.set 方法

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

存儲數(shù)據(jù)到緩存中,并制定過期時間和當(dāng)Key存在時是否覆蓋。
key 鎖的名字
value 鎖的內(nèi)容
nxxx的值只能取NX或者XX,如果取NX,則只有當(dāng)key不存在是才進(jìn)行set,如果取XX,則只有當(dāng)key已經(jīng)存在時才進(jìn)行set
expx的值只能取EX或者PX,代表數(shù)據(jù)過期時間的單位,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);
        }
    }
}

五、在業(yè)務(wù)上添加鎖

public Result loginByWxLock(String openId){
        // 更新點(diǎn)贊數(shù)量
        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();

        // 統(tǒng)計(jì)信息
        int loginSuccessNum = 0;
        int loginFailedNum = 0;
        for (Result result : results) {
            if (result.getCode() == 0) {
                loginSuccessNum++;
            }else{
                loginFailedNum++;
            }
        }
        System.out.println("登錄成功總數(shù):" + loginSuccessNum);
        System.out.println("登錄失敗總數(shù):" + loginFailedNum);
    }

測試結(jié)果


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

更多精彩內(nèi)容

關(guān)注我們

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容