分佈式鎖與redis 分佈式鎖實現

分佈式鎖

概念

  1. 任何一個系統都無法同時滿足一致性(Consistency),可用性(Availability),分區容錯性(Partition tolerance), 只能同事滿足2個;
  2. 分佈式鎖就是爲了解決數據一致性問題.

悲觀鎖和樂觀鎖

悲觀鎖:
  • 總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次拿數據都會上鎖,這樣別人想拿這個數據就會阻塞,知道鎖被釋放
  • 悲觀鎖多用於多寫場景,關係型數據庫中很多實用了這種悲觀鎖機制
  • 實現:
    1. redis 實現鎖機制

樂觀鎖

  • 總是假設最好的情況,即每次去拿數據的時候都認爲別的線程不會去修改,所以不會上鎖,但是在更新數據的時候會判斷在此期間有沒有其它線程更新了這個數據,可以用版本號機制和CAS算法來實現;
  • 樂觀鎖多用戶多讀場景,提高吞吐量,比如數據庫提供的write_condition機制
  • 實現:
    1. 數據庫添加版本號字段: 一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
    2. CAS 算法

應用場景

  • 涉及到多個實例進程操作同一份數據時就需要用到鎖機制,比如: 下單,修改庫存,更新緩存等

分佈式鎖的特性

  1. 分佈式環境下同一時刻只能被單個線程獲取;
  2. 已經獲取鎖的進程在使用中不需要再次獲取;
  3. 異常或者超時自動釋放鎖,避免死鎖
  4. 高性能,分佈式環境下必須性能好;

實現方式

  1. 基於redis 緩存實現;
  2. 基於zookeeper 臨時順序節點實現;
  3. 基於數據庫行鎖實現;

redis 分佈式鎖

  • 採用springboot + redis
  1. redis 配置
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    lettuce:
      pool:
        # 最大活躍鏈接數 默認8
        max-active: 100
        # 最大空閒連接數 默認8
        max-idle: 10
        # 最小空閒連接數 默認0
        min-idle: 5
    timeout: 30000
  1. 分佈式鎖接口 DistributedLock
public interface DistributedLock {


    String lock(String name);

    /**
     * 加鎖,有阻塞
     * @param name
     * @param expire
     * @param timeout
     * @return
     */
    String lock(String name, long expire, long timeout);


    String tryLock(String name);

    /**
     * 加鎖,無阻塞
     * @param name
     * @param expire
     * @return
     */
    String tryLock(String name, long expire);

    /**
     * 解鎖
     * @param name
     * @param token
     * @return
     */
    boolean unlock(String name, String token);

    void close();

}
  1. 分佈式鎖實現 RedisDistributedLockImpl
@Service
@Slf4j
public class RedisDistributedLockImpl implements DistributedLock{

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private RedisLock redisLock;

    @PostConstruct
    public void init(){
        redisLock = new RedisLock(stringRedisTemplate);
    }

    public int getCount() {
        return redisLock.getCount();
    }

    @Override
    public String lock(String name){
        return redisLock.lock(name);
    }

    /**
     * 加鎖,有阻塞
     * @param name
     * @param expire
     * @param timeout
     * @return
     */
    @Override
    public String lock(String name, long expire, long timeout){
        return redisLock.lock(name, expire, timeout);
    }

    @Override
    public String tryLock(String name) {
        return redisLock.tryLock(name);
    }

    /**
     * 加鎖,無阻塞
     * @param name
     * @param expire
     * @return
     */
    @Override
    public String tryLock(String name, long expire) {
        return redisLock.tryLock(name, expire);
    }

    /**
     * 解鎖
     * @param name
     * @param token
     * @return
     */
    @Override
    public boolean unlock(String name, String token) {
        return redisLock.unlock(name, token);
    }

    @Override
    public void close() {
        redisLock.close();
    }
}
  1. redis 鎖具體邏輯 RedisLock
@Slf4j
public class RedisLock {

    /**
     * 解鎖腳本,原子操作
     */
    private static final String unlockScript =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
                    + "then\n"
                    + "    return redis.call(\"del\",KEYS[1])\n"
                    + "else\n"
                    + "    return 0\n"
                    + "end";

    private StringRedisTemplate redisTemplate;
    private long timeout = 60000;
    private long expire = 300000;
    private String PREFIX = "lock:";
    private int count = 0;

    public RedisLock(StringRedisTemplate redisTemplate) {
        init(redisTemplate, this.expire, this.timeout);
    }

    public RedisLock(StringRedisTemplate redisTemplate, long expire, long timeout) {
        init(redisTemplate, expire, timeout);
    }

    public void init(StringRedisTemplate redisTemplate, long expire, long timeout) {
        this.redisTemplate = redisTemplate;
        this.expire = expire;
        this.timeout = timeout;
    }

    public int getCount() {
        return count;
    }

    public String lock(String name){
        return this.lock(name, this.expire, this.timeout);
    }

    /**
     * 加鎖,有阻塞
     * @param name
     * @param expire
     * @param timeout
     * @return
     */
    public String lock(String name, long expire, long timeout){
        long startTime = System.currentTimeMillis();
        String token;
        do{
            token = tryLock(name, expire);
            log.debug("lock token:{}", token);
            if(token == null) {
                if((System.currentTimeMillis()-startTime) > (timeout-10))
                    return token;
                try {
                    Thread.sleep(500); //try 10 per millis
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }while(token==null);

        return token;
    }


    public String tryLock(String name) {
        return this.tryLock(name, this.expire);
    }

    /**
     * 加鎖,無阻塞
     * @param name
     * @param expire
     * @return
     */
    public String tryLock(String name, long expire) {
        String token = UUID.randomUUID().toString();
        String key = PREFIX + name;
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try{
            Boolean result = conn.set(key.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")),
                    Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
            if(result!=null && result) {
                count++;
                return token;
            }
        } catch (Exception e){
            log.error("fail to tryLock name:{}", name);
            e.printStackTrace();
        } finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }
        return null;
    }

    /**
     * 解鎖
     * @param name
     * @param token
     * @return
     */
    public boolean unlock(String name, String token) {
        String key = PREFIX + name;
        byte[][] keysAndArgs = new byte[2][];
        keysAndArgs[0] = key.getBytes(Charset.forName("UTF-8"));
        keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try {
            Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
            if(result!=null && result>0) {
                count--;
                return true;
            }
        } catch (Exception e){
            log.error("fail to unlock name:{}, token:{}", key, token);
            e.printStackTrace();
        } finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }

        return false;
    }

    public void close()
    {
        log.info("close connect");
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        RedisConnectionUtils.releaseConnection(conn, factory);
    }

}
  1. redis 分佈式鎖測試
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SosApplication.class)
@Slf4j
public class RedisTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private DistributedLock lock;

    private Executor executor = Executors.newFixedThreadPool(100);

    private int concurrenceResult = 0;

    @Before
    public void setUp() {

    }

    @After
    public void tearDown() {
    }

    @Test
    public void empty() {

    }

    @Test
    public void deleteRedisOrders() {
        while (doDelete("order::*", 5000) == 5000) {
            log.info("another loop...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private int doDelete(String match, int n) {
        AtomicInteger res = new AtomicInteger();
        redisTemplate.execute((RedisCallback<?>) (connection) -> {
            int i = 0;
            Cursor<byte[]> cursors = null;
            log.info("start delete ...");
            try {
                cursors = connection.scan(ScanOptions.scanOptions().match(match).count(n).build());
                while (cursors.hasNext()) {
                    byte[] key = cursors.next();
                    connection.del(key);
                    log.info("delete ---> {}", new String(key));
                    i++;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                log.info("total deleted {}", i);
                res.set(i);
                if (cursors != null) {
                    try {
                        cursors.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

            return null;
        });
        return res.get();
    }


    @Test
    public void testLock() {
        String token = lock.lock("test");
        log.debug("token:{}", token);
        Assert.notNull(token, "lock fail");
        boolean rc = lock.unlock("test", token);
        Assert.isTrue(rc, "unlock fail");
    }

    @Test
    public void testLockWithConcurrence() {
        concurrenceResult = 0;
        int threadNum= 30;
        int loopNum = 100;
        int result = threadNum * loopNum;
        CountDownLatch latch = new CountDownLatch(threadNum);
        log.info("testThreadLock...");
        for(int i=0; i< threadNum; i++) {
            executor.execute(() -> {
                doWorkWithLock(loopNum, latch);
            });
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("testThreadLock result:{}", concurrenceResult);
        Assert.isTrue(concurrenceResult==result, "testLockWithConcurrence result should be " + result);
    }

    private void doWork(int n, CountDownLatch latch){
        try {
            Random rand = new Random();
            for(int i=0; i<n; i++) {
                long randNum = rand.nextInt(20);
                log.debug("doWork sleep:{}, thread:{}", randNum, Thread.currentThread().getName());
                concurrenceResult++;
                Thread.sleep(randNum);
            }
            if(latch != null) {
                latch.countDown();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    private void doWorkWithLock(int n, CountDownLatch latch){
        String name = "lockName";
        String token = lock.lock(name);
        long s = new Date().getTime();
        log.info("doWorkWithLock lock name:{}, token:{}, thread:{}", name, token, Thread.currentThread().getName());
        doWork(n, latch);
        boolean rc = lock.unlock(name, token);
        long e = new Date().getTime();
        log.info("doWorkWithLock unlock name:{}, token:{}, rc:{}, times:{}, thread:{}", name, token, rc, e-s, Thread.currentThread().getName());
    }
}
  1. 使用案例
        //使用分佈式鎖獲取log,防止多進程運行時重複獲取
        String token = lock.tryLock(LOCK_NAME);
        if (token != null) {
            try {
                // 具體業務
            } finally {
                lock.unlock(LOCK_NAME, token);
            }
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章