Redis锁, SETNX, lua脚本和eval函数, CyclicBarrier栅栏
来源:互联网 发布:马哥linux全套视频 编辑:程序博客网 时间:2024/06/04 19:33
参考:http://blog.csdn.net/wtopps/article/details/70768062
模拟多线程并发:https://www.cnblogs.com/dolphin0520/p/3920397.html
http://flysnowxf.iteye.com/blog/1188496
问题1:
spring redis和redis包在设置key值的时候,都是先调用setnx设置值,成功就返回1,然后通过Expire设置超时时间,这样会出现一个
问题假如setnx成功,但是expire的时候,失败了,那么该值就会一直存在,这样会造成大的问题,这个问题怎么解决呢?
我们可以通过redis lua脚本,让设置值和设置超时时间在redis服务端一次执行,就不会造成前面描述的问题。
http://blog.csdn.net/mr_smile2014/article/details/73849573
问题2:
因为 SetNX 不具备设置过期时间的功能,所以我们需要借助 Expire 来设置,同时我们需要把两者用 Multi/Exec 包裹起来以确保请求的原子性,以免 SetNX 成功了 Expire 却失败了。 可惜还有问题:当多个请求到达时,虽然只有一个请求的 SetNX 可以成功,但是任何一个请求的 Expire 却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求比较密集的话,那么过期时间会一直被刷新,导致锁一直有效。于是乎我们需要在保证原子性的同时,有条件的执行 Expire。
其实 Redis 已经考虑到了大家的疾苦,从 2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。
"return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) "
问题3:
如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新过程中,锁就失效了,此时另一个请求会获取锁,但前一个请求在缓存更新完毕的时候,如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况,所以我们在创建锁的时候需要引入一个随机值。
"if (redis.call('GET', KEYS[1]) == ARGV[1]) " + "then return redis.call('DEL',KEYS[1]) " + "else " + "return 0 " + "end"上面没用随机值,用生成的授权码效果也是一样的。
package cn.tdw.service;import cn.tdw.exception.OauthErrorCode;import cn.tdw.util.RedisLock;import com.tuandai.ms.apiutils.exception.AppBusinessException;import org.apache.commons.lang3.StringUtils;import org.junit.Test;import org.junit.runner.RunWith;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.data.redis.core.script.RedisScript;import org.springframework.data.redis.serializer.RedisSerializer;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import java.util.Collections;import java.util.UUID;import java.util.concurrent.*;/** * Date: 2017/11/27 9:38 * * @author huangkaijie * @version V1.0 * @since JDK 1.8.0_131 */@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class concurrentTest { public final Logger logger = LoggerFactory.getLogger(concurrentTest.class); // Redis获取锁:setnx+设置过期时间的执行命令 private final static String LUA_SCRIPT_LOCK = "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) "; private static RedisScript<String> scriptLock = new DefaultRedisScript<String>(LUA_SCRIPT_LOCK, String.class); //Redis释放锁:通过value判定 private static final String LUA_SCRIPT_UNLOCK = "if (redis.call('GET', KEYS[1]) == ARGV[1]) " + "then return redis.call('DEL',KEYS[1]) " + "else " + "return 0 " + "end"; private static RedisScript<Long> scriptUnlock = new DefaultRedisScript<Long>(LUA_SCRIPT_UNLOCK, Long.class); @Autowired private RedisTemplate<String, String> redisTemplate; @Test public void concurrentTest() { int N = 4; ExecutorService executorService = Executors.newCachedThreadPool(); CyclicBarrier barrier = new CyclicBarrier(N); for (int i = 0; i < N; i++) { executorService.execute(new doServiceRunnable(barrier)); } executorService.shutdown(); while (!executorService.isTerminated()) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("所有线程执行完毕..."); } private class doServiceRunnable implements Runnable { private CyclicBarrier cyclicBarrier; public doServiceRunnable(CyclicBarrier cyclicBarrier) { this.cyclicBarrier = cyclicBarrier; } @Override public void run() { System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据..."); try { Thread.sleep(5000); //以睡眠来模拟写入数据操作 cyclicBarrier.await(); System.out.println("线程开始执行逻辑处理时间" + Thread.currentThread().getName() + " " + System.currentTimeMillis()); this.doLogic(); System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println("所有线程写入完毕,继续处理其他任务..."); } public void doLogic() { boolean lock = false; RedisLock redisLock = null; String code = "code123456"; // 由一定的规则生成 try { redisLock = lock(code); lock = (redisLock != null); if (lock) {//redis锁并发控制 String token = this.generateAccessToken("client_id_123", "client_secret_456"); //保存缓存的access_token到redis(授权码已从redis删除,生成的access_token可能因为网络中断或其他原因没传回去,故缓存到redis10分钟) redisTemplate.opsForValue().set("TEST_TOKEN_" + code, token, 10, TimeUnit.MINUTES); logger.info("返回新的token=" + token); } } catch (Exception e) { throw e; } finally { if (lock) { //删除redis中的授权码 try {// redisTemplate.delete("TEST_TOKEN_" + code); } catch (Exception e) { logger.error("删除redis授权码失败,code:" + code); } unlock(redisLock); } } if (!lock) { throw new AppBusinessException(OauthErrorCode.REQUEST_QUICKLY_ERROR); } } //获取锁 private RedisLock lock(String code) { String flagKey = "TEST_REDIS_CONCURRENT_LOCK_" + code;//并发标志键值 String uuid = UUID.randomUUID().toString(); String expireTime = "1000";//过期时间/毫秒 String execute = redisTemplate.execute(scriptLock, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(flagKey), uuid, expireTime); if (execute != null && execute.equals("OK")) { logger.info("线程" + Thread.currentThread().getName() + "===============>获取锁成功,授权码code:" + code); return new RedisLock(flagKey, uuid); } return null; } //释放锁 private void unlock(RedisLock redisLock) { redisTemplate.execute(scriptUnlock, redisTemplate.getStringSerializer(), (RedisSerializer<Long>) redisTemplate.getKeySerializer(), Collections.singletonList(redisLock.getKey()), redisLock.getValue()); } /** * 生成唯一AccessToken * * @param client_id * @param client_secret * @return */ private String generateAccessToken(String client_id, String client_secret) { String md5Str = md5(client_id + client_secret + System.currentTimeMillis()); return md5Str.substring(md5Str.length() - 12, md5Str.length()) + UUID.randomUUID().toString().replace("-", ""); } /** * 通用md5加密方法 * * @param text * @return */ private String md5(String text) { if (text == null || StringUtils.isEmpty(text.trim())) return ""; try { StringBuilder sb = new StringBuilder(); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(text.getBytes(StandardCharsets.UTF_8)); for (byte b : md.digest()) { int n = b; if (n < 0) n += 256; if (n < 16) sb.append("0"); sb.append(Integer.toHexString(n)); } return sb.toString(); } catch (Exception e) { logger.error(e.getMessage(), e.getCause()); } return null; } }}
运行结果:
线程pool-3-thread-3正在写入数据...线程pool-3-thread-4正在写入数据...线程pool-3-thread-2正在写入数据...线程开始执行逻辑处理时间pool-3-thread-41511754032962线程开始执行逻辑处理时间pool-3-thread-11511754032962线程开始执行逻辑处理时间pool-3-thread-31511754032962线程开始执行逻辑处理时间pool-3-thread-215117540329622017-11-27 11:40:33.052 INFO 23512 --- [pool-3-thread-1] cn.tdw.service.concurrentTest : 线程pool-3-thread-1===============>获取锁成功,授权码code:code123456Exception in thread "pool-3-thread-3" com.tuandai.ms.apiutils.exception.AppBusinessException: 请求授权令牌过于频繁at cn.tdw.service.concurrentTest$doServiceRunnable.doLogic(concurrentTest.java:128)at cn.tdw.service.concurrentTest$doServiceRunnable.run(concurrentTest.java:90)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:748)Exception in thread "pool-3-thread-4" com.tuandai.ms.apiutils.exception.AppBusinessException: 请求授权令牌过于频繁at cn.tdw.service.concurrentTest$doServiceRunnable.doLogic(concurrentTest.java:128)at cn.tdw.service.concurrentTest$doServiceRunnable.run(concurrentTest.java:90)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:748)Exception in thread "pool-3-thread-2" com.tuandai.ms.apiutils.exception.AppBusinessException: 请求授权令牌过于频繁at cn.tdw.service.concurrentTest$doServiceRunnable.doLogic(concurrentTest.java:128)at cn.tdw.service.concurrentTest$doServiceRunnable.run(concurrentTest.java:90)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:748)2017-11-27 11:40:33.062 INFO 23512 --- [pool-3-thread-1] cn.tdw.service.concurrentTest : 返回新的token=4a400d4f4f0e9f2212dd33174fdd843232b35585b612线程pool-3-thread-1写入数据完毕,等待其他线程写入完毕所有线程写入完毕,继续处理其他任务...所有线程执行完毕...
- Redis锁, SETNX, lua脚本和eval函数, CyclicBarrier栅栏
- 闭锁CountDownLatch和栅栏CyclicBarrier
- redis setnx 实现分布式锁和单机锁
- redis分布式锁-SETNX实现
- redis分布式锁-SETNX实现
- redis分布式锁-SETNX实现
- redis分布式锁-SETNX实现
- 栅栏CyclicBarrier
- CyclicBarrier 栅栏
- 【Redis】 redis setnx命令实现分布式锁
- 使用Redis SETNX 命令实现分布式锁
- 谈谈Redis的SETNX分布式锁
- Redis SETNX 命令实现分布式锁
- 使用Redis SETNX 命令实现分布式锁
- Redis SETNX命令实现分布式锁
- 有关redis中setnx实现锁
- 使用Redis SETNX 命令实现分布式锁
- 使用redis的setnx制作排他锁
- stf在Linux上的安装过程(实践中一步一步操作总结的)
- HandleBars中自定义helper方法
- Centos 6.5升级Git到Git2.1.2的步骤
- LMK(Low Memory Killer)
- [C++]实现顺序表和单链表
- Redis锁, SETNX, lua脚本和eval函数, CyclicBarrier栅栏
- javaweb简单的登录注册功能实现
- 微信用户提现不能到账,显示NO_AUTH | 产品权限验证失败,请查看您当前是否具有该产品的权限
- 关于python自增运算(千万不要用++i,不然程序崩掉)
- 如何做一个对账系统
- Git学习记录
- jquery 删除功能代码
- 基于express框架快速搭建web项目
- BigDecimal.setScale用法总结