一 问题背景
我们做的是医疗信息化系统, 在系统中一条患者信息对医院中当前科室中的所有诊断医生是可见的, 当有一个诊断医生点击按钮处理该数据时, 数据的状态发生了变化, 其他的医生就不可以再处理此患者的数据了. 我们开始的做法是, 在医生点击按钮时先去后台数据库获取当前数据状态, 根据状态判断数据是否可以操作, 如果可以操作, 则修改数据状态, 进行业务逻辑处理, 否则提示数据已被其他人处理, 不能处理.
二 问题分析
按照上边的业务逻辑, 我们画个图分析, 如下图
在上图中, 如果用户 A 和 B 同时向数据库发起请求获取数据状态, 数据库返回 wait,A 和 B 都拿到了相同的状态, 判断是可以操作数据的, 这时他们处理数据. A 用户处理完成后提交了数据, 数据库状态变为 done, 记录此数据的处理人为 A. 由于 B 用户也可以处理数据, 所以他也提交数据, 这时数据的操作人记录为了 B. 有人会说, 在 A 和 B 提交数据修改状态时再做一个状态的判断, 这种也难以避免最开始的获取状态的问题, 即使这一步状态获取到了, 提示后边的人不能修改, 这又会产生系统不友好的问题(我操作了半天, 到最后你告诉我不能处理, 我白忙活了). 以上问题产生的主要原因就是在多线程情况下对共享数据的资源竞争处理不当, 我们需要保证数据的唯一性, 即在某一时刻, 只能有一个线程独享数据资源.
三 问题解决
如何解决呢? 分布式锁, 分布式锁有多种实现方式, 本文我们用 Redis 实现. 由于 Redis 是单线程的, 所以一次只能处理一个请求, 并将资源分配给这个请求, 我们称加 锁. 如下图
多线程情况下, Redis 只会处理其中的一个, 其他的暂时等待. 如上图当 A 和 B 同时发出请求时, Redis 接受并处理 A 请求, 此时 B 请求排队等待, 等到 A 请求处理完后再处理 B 请求. 此时 Redis 已经将资源 (lock) 分配给了 A,A 请求数据库, B 请求没有获取到资源直接返回不在请求数据库. 这样就保证了数据库共享数据被唯一资源使用. 代码简单实现
- public class RedisLock {
- private static final String GET_RESULT = "OK";
- private static final String RELEASE_RESULT = "1";
- private static final String SET_IF_NOT_EXIST = "NX";
- private static final String SET_WITH_EXPIRE_TIME = "PX";
- /**
- * 获取 Redis 锁
- * @param jedis Redis 客户端
- * @param lockKey 锁标识 key
- * @param requestId 锁的持有者, 加锁的请求
- * @param expireTime 锁过期时间
- * @return
- */
- public static boolean getLock(Jedis jedis, String lockKey, String requestId, int expireTime){
- //SET_IF_NOT_EXIST 当 key 不存在时 才处理
- //SET_WITH_EXPIRE_TIME 设置过期时间 时间由 expireTime 决定
- String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
- if (GET_RESULT.equals(result)) {
- return true;
- }
- return false;
- }
- /**
- * 释放锁
- * @param jedis
- * @param lockKey
- * @param requestId
- * @return
- */
- public static boolean releaseLock(Jedis jedis, String lockKey, String requestId){
- // 方式 1
- // if (jedis.get(lockKey).equals(requestId)) {// 校验当前锁的持有人与但概念请求是否相同
- // 执行在这里时, 如果锁被其它请求重新获取到了, 此时就不该删除了
- // jedis.del(lockKey);
- // }
- // 方式 2
- // eval() 方法会交给 Redis 服务端执行, 减少了从服务端再到客户端处理的过程
- // 赋值 KEYS[1] = lockKey ARGV[1] = requestId
- String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
- Object releaseResult = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
- if (RELEASE_RESULT.equals(releaseResult.toString())) {
- return true;
- }
- return false;
- }
- }
四 测试锁机制
测试并发我们可以使用一些软件, 比如 Jmeter, 本文我们写个方法测试
- public static void main(String[] args) {
- // 要创建的线程的数量
- CountDownLatch looker = new CountDownLatch(1);
- CountDownLatch latch = new CountDownLatch(10);
- final String key = "lockKey";
- for(int i=0; i < latch.getCount(); i++){
- Jedis jedis = new Jedis();
- UUID uuid = UUID.randomUUID();
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- looker.await();
- System.out.println(Thread.currentThread().getName()+"竞争资源, 获取锁");
- boolean getResult = getLock(jedis, key, uuid.toString(), 5000);
- if(getResult){
- System.out.println(Thread.currentThread().getName()+"获取到了锁, 处理业务, 用时 3 秒");
- Thread.sleep(3000);
- boolean releaseResult = releaseLock(jedis, key, uuid.toString());
- if(releaseResult){
- System.out.println(Thread.currentThread().getName()+"业务处理完毕, 释放锁");
- }
- }else{
- System.out.println(Thread.currentThread().getName()+"竞争资源失败, 未获取到锁");
- }
- latch.countDown();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- thread.start();
- }
- try {
- System.out.println("准备, 5 秒后开始");
- Thread.sleep(5000);
- looker.countDown(); // 发令 let all threads proceed
- latch.await(); // // wait for all to finish
- System.out.println("结束");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
可以看到控制台上输出的结果
来源: https://www.cnblogs.com/love-wzy/p/10190514.html