分布式锁与Redis 分布式锁实现

与世无争的帅哥 提交于 2021-01-26 08:31:56

分布式锁

概念

  1. 任何一个系统都无法同时满足一致性(Consistency),可用性(Availability),分区容错性(Partition tolerance), 只能同时满足2个;

  2. 分布式锁就是为了解决数据一致性问题.

悲观锁和乐观锁

悲观锁:
  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据都会上锁,这样别人想拿这个数据就会阻塞,知道锁被释放

  • 悲观锁多用于多写场景,关系型数据库中很多使用了这种悲观锁机制

  • 实现:

  1. redis 实现锁机制

乐观锁

  • 总是假设最好的情况,即每次去拿数据的时候都认为别的线程不会去修改,所以不会上锁,但是在更新数据的时候会判断在此期间有没有其它线程更新了这个数据,可以用版本号机制和CAS算法来实现;

  • 乐观锁多用户多读场景,提高吞吐量,比如数据库提供的write_condition机制

  • 实现:

  1. 数据库添加版本号字段: 一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

  2. CAS 算法

应用场景

  • 涉及到多个实例进程操作同一份数据时就需要用到锁机制,比如: 下单,修改库存,更新缓存等

分布式锁的特性

  1. 分布式环境下同一时刻只能被单个线程获取;

  2. 已经获取锁的进程在使用中不需要再次获取;

  3. 异常或者超时自动释放锁,避免死锁

  4. 高性能,分布式环境下必须性能好;

实现方式

  1. 基于redis 缓存实现;

  2. 基于zookeeper 临时顺序节点实现;

  3. 基于数据库行锁实现;

redis 分布式锁

  • 采用springboot + redis

  1. redis 配置

  
  
  1. spring:

  2. redis:

  3. host: 127.0.0.1

  4. port: 6379

  5. password:

  6. database: 0

  7. lettuce:

  8. pool:

  9. # 最大活跃链接数 默认8

  10. max-active: 100

  11. # 最大空闲连接数 默认8

  12. max-idle: 10

  13. # 最小空闲连接数 默认0

  14. min-idle: 5

  15. timeout: 30000

  1. 分布式锁接口 DistributedLock

  
  
  1. public interface DistributedLock {



  2. String lock(String name);


  3. /**

  4. * 加锁,有阻塞

  5. * @param name

  6. * @param expire

  7. * @param timeout

  8. * @return

  9. */

  10. String lock(String name, long expire, long timeout);



  11. String tryLock(String name);


  12. /**

  13. * 加锁,无阻塞

  14. * @param name

  15. * @param expire

  16. * @return

  17. */

  18. String tryLock(String name, long expire);


  19. /**

  20. * 解锁

  21. * @param name

  22. * @param token

  23. * @return

  24. */

  25. boolean unlock(String name, String token);


  26. void close();


  27. }

  1. 分布式锁实现 RedisDistributedLockImpl

  
  
  1. @Service

  2. @Slf4j

  3. public class RedisDistributedLockImpl implements DistributedLock{


  4. @Autowired

  5. private StringRedisTemplate stringRedisTemplate;

  6. private RedisLock redisLock;


  7. @PostConstruct

  8. public void init(){

  9. redisLock = new RedisLock(stringRedisTemplate);

  10. }


  11. public int getCount() {

  12. return redisLock.getCount();

  13. }


  14. @Override

  15. public String lock(String name){

  16. return redisLock.lock(name);

  17. }


  18. /**

  19. * 加锁,有阻塞

  20. * @param name

  21. * @param expire

  22. * @param timeout

  23. * @return

  24. */

  25. @Override

  26. public String lock(String name, long expire, long timeout){

  27. return redisLock.lock(name, expire, timeout);

  28. }


  29. @Override

  30. public String tryLock(String name) {

  31. return redisLock.tryLock(name);

  32. }


  33. /**

  34. * 加锁,无阻塞

  35. * @param name

  36. * @param expire

  37. * @return

  38. */

  39. @Override

  40. public String tryLock(String name, long expire) {

  41. return redisLock.tryLock(name, expire);

  42. }


  43. /**

  44. * 解锁

  45. * @param name

  46. * @param token

  47. * @return

  48. */

  49. @Override

  50. public boolean unlock(String name, String token) {

  51. return redisLock.unlock(name, token);

  52. }


  53. @Override

  54. public void close() {

  55. redisLock.close();

  56. }

  57. }

  1. redis 锁具体逻辑 RedisLock

  
  
  1. @Slf4j

  2. public class RedisLock {


  3. /**

  4. * 解锁脚本,原子操作

  5. */

  6. private static final String unlockScript =

  7. "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"

  8. + "then\n"

  9. + " return redis.call(\"del\",KEYS[1])\n"

  10. + "else\n"

  11. + " return 0\n"

  12. + "end";


  13. private StringRedisTemplate redisTemplate;

  14. private long timeout = 60000;

  15. private long expire = 300000;

  16. private String PREFIX = "lock:";

  17. private int count = 0;


  18. public RedisLock(StringRedisTemplate redisTemplate) {

  19. init(redisTemplate, this.expire, this.timeout);

  20. }


  21. public RedisLock(StringRedisTemplate redisTemplate, long expire, long timeout) {

  22. init(redisTemplate, expire, timeout);

  23. }


  24. public void init(StringRedisTemplate redisTemplate, long expire, long timeout) {

  25. this.redisTemplate = redisTemplate;

  26. this.expire = expire;

  27. this.timeout = timeout;

  28. }


  29. public int getCount() {

  30. return count;

  31. }


  32. public String lock(String name){

  33. return this.lock(name, this.expire, this.timeout);

  34. }


  35. /**

  36. * 加锁,有阻塞

  37. * @param name

  38. * @param expire

  39. * @param timeout

  40. * @return

  41. */

  42. public String lock(String name, long expire, long timeout){

  43. long startTime = System.currentTimeMillis();

  44. String token;

  45. do{

  46. token = tryLock(name, expire);

  47. log.debug("lock token:{}", token);

  48. if(token == null) {

  49. if((System.currentTimeMillis()-startTime) > (timeout-10))

  50. return token;

  51. try {

  52. Thread.sleep(500); //try 10 per millis

  53. } catch (InterruptedException e) {

  54. e.printStackTrace();

  55. }

  56. }

  57. }while(token==null);


  58. return token;

  59. }



  60. public String tryLock(String name) {

  61. return this.tryLock(name, this.expire);

  62. }


  63. /**

  64. * 加锁,无阻塞

  65. * @param name

  66. * @param expire

  67. * @return

  68. */

  69. public String tryLock(String name, long expire) {

  70. String token = UUID.randomUUID().toString();

  71. String key = PREFIX + name;

  72. RedisConnectionFactory factory = redisTemplate.getConnectionFactory();

  73. RedisConnection conn = factory.getConnection();

  74. try{

  75. Boolean result = conn.set(key.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")),

  76. Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);

  77. if(result!=null && result) {

  78. count++;

  79. return token;

  80. }

  81. } catch (Exception e){

  82. log.error("fail to tryLock name:{}", name);

  83. e.printStackTrace();

  84. } finally {

  85. RedisConnectionUtils.releaseConnection(conn, factory);

  86. }

  87. return null;

  88. }


  89. /**

  90. * 解锁

  91. * @param name

  92. * @param token

  93. * @return

  94. */

  95. public boolean unlock(String name, String token) {

  96. String key = PREFIX + name;

  97. byte[][] keysAndArgs = new byte[2][];

  98. keysAndArgs[0] = key.getBytes(Charset.forName("UTF-8"));

  99. keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));

  100. RedisConnectionFactory factory = redisTemplate.getConnectionFactory();

  101. RedisConnection conn = factory.getConnection();

  102. try {

  103. Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);

  104. if(result!=null && result>0) {

  105. count--;

  106. return true;

  107. }

  108. } catch (Exception e){

  109. log.error("fail to unlock name:{}, token:{}", key, token);

  110. e.printStackTrace();

  111. } finally {

  112. RedisConnectionUtils.releaseConnection(conn, factory);

  113. }


  114. return false;

  115. }


  116. public void close()

  117. {

  118. log.info("close connect");

  119. RedisConnectionFactory factory = redisTemplate.getConnectionFactory();

  120. RedisConnection conn = factory.getConnection();

  121. RedisConnectionUtils.releaseConnection(conn, factory);

  122. }


  123. }

  1. redis 分布式锁测试

  
  
  1. @RunWith(SpringRunner.class)

  2. @SpringBootTest(classes = SosApplication.class)

  3. @Slf4j

  4. public class RedisTest {

  5. @Autowired

  6. private RedisTemplate redisTemplate;


  7. @Autowired

  8. private DistributedLock lock;


  9. private Executor executor = Executors.newFixedThreadPool(100);


  10. private int concurrenceResult = 0;


  11. @Before

  12. public void setUp() {


  13. }


  14. @After

  15. public void tearDown() {

  16. }


  17. @Test

  18. public void empty() {


  19. }


  20. @Test

  21. public void deleteRedisOrders() {

  22. while (doDelete("order::*", 5000) == 5000) {

  23. log.info("another loop...");

  24. try {

  25. Thread.sleep(1000);

  26. } catch (InterruptedException e) {

  27. e.printStackTrace();

  28. }

  29. }

  30. }


  31. private int doDelete(String match, int n) {

  32. AtomicInteger res = new AtomicInteger();

  33. redisTemplate.execute((RedisCallback<?>) (connection) -> {

  34. int i = 0;

  35. Cursor<byte[]> cursors = null;

  36. log.info("start delete ...");

  37. try {

  38. cursors = connection.scan(ScanOptions.scanOptions().match(match).count(n).build());

  39. while (cursors.hasNext()) {

  40. byte[] key = cursors.next();

  41. connection.del(key);

  42. log.info("delete ---> {}", new String(key));

  43. i++;

  44. }

  45. } catch (Exception e) {

  46. e.printStackTrace();

  47. } finally {

  48. log.info("total deleted {}", i);

  49. res.set(i);

  50. if (cursors != null) {

  51. try {

  52. cursors.close();

  53. } catch (IOException e) {

  54. e.printStackTrace();

  55. }

  56. }

  57. }


  58. return null;

  59. });

  60. return res.get();

  61. }



  62. @Test

  63. public void testLock() {

  64. String token = lock.lock("test");

  65. log.debug("token:{}", token);

  66. Assert.notNull(token, "lock fail");

  67. boolean rc = lock.unlock("test", token);

  68. Assert.isTrue(rc, "unlock fail");

  69. }


  70. @Test

  71. public void testLockWithConcurrence() {

  72. concurrenceResult = 0;

  73. int threadNum= 30;

  74. int loopNum = 100;

  75. int result = threadNum * loopNum;

  76. CountDownLatch latch = new CountDownLatch(threadNum);

  77. log.info("testThreadLock...");

  78. for(int i=0; i< threadNum; i++) {

  79. executor.execute(() -> {

  80. doWorkWithLock(loopNum, latch);

  81. });

  82. }


  83. try {

  84. latch.await();

  85. } catch (InterruptedException e) {

  86. e.printStackTrace();

  87. }

  88. log.info("testThreadLock result:{}", concurrenceResult);

  89. Assert.isTrue(concurrenceResult==result, "testLockWithConcurrence result should be " + result);

  90. }


  91. private void doWork(int n, CountDownLatch latch){

  92. try {

  93. Random rand = new Random();

  94. for(int i=0; i<n; i++) {

  95. long randNum = rand.nextInt(20);

  96. log.debug("doWork sleep:{}, thread:{}", randNum, Thread.currentThread().getName());

  97. concurrenceResult++;

  98. Thread.sleep(randNum);

  99. }

  100. if(latch != null) {

  101. latch.countDown();

  102. }

  103. } catch (InterruptedException e) {

  104. e.printStackTrace();

  105. }

  106. }



  107. private void doWorkWithLock(int n, CountDownLatch latch){

  108. String name = "lockName";

  109. String token = lock.lock(name);

  110. long s = new Date().getTime();

  111. log.info("doWorkWithLock lock name:{}, token:{}, thread:{}", name, token, Thread.currentThread().getName());

  112. doWork(n, latch);

  113. boolean rc = lock.unlock(name, token);

  114. long e = new Date().getTime();

  115. log.info("doWorkWithLock unlock name:{}, token:{}, rc:{}, times:{}, thread:{}", name, token, rc, e-s, Thread.currentThread().getName());

  116. }

  117. }

  1. 使用案例

  
  
  1. //使用分布式锁获取log,防止多进程运行时重复获取

  2. String token = lock.tryLock(LOCK_NAME);

  3. if (token != null) {

  4. try {

  5. // 具体业务

  6. } finally {

  7. lock.unlock(LOCK_NAME, token);

  8. }

  9. }


本文分享自微信公众号 - 码小山(gh_d9b3dc861714)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!