分布式锁与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);
            }
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章