高阶玩家需要掌握的

说说自己的理解,结合背景、业务、代码谈技术

  • 分布式系统

结合自己之前做过的业务系统,有这样一个业务场景,也是比较常见的,销售、代理商需要登录系统去给商户开通相应的产品功能;

这个时候比较容易想到的几个业务场景,提单、拆单、工单审批、审批的过程中还需要一系列的RPC、消息交互等等

如果这么多个场景耦合在一起,在一个系统中能实现么?答案是,当然可以,刚毕业那会的项目可能就是都这么做的;

耦合性比较高,业务之间紧密相连,这样做的好处就是事务处理就简单了,本地事务,利用数据库的事务就能帮你解决你会遇到的所有事务问题;

但是随之而来的就是高风险,万一哪个功能点出现问题,系统就挂了,代码会极其多,量变就会产生质变,一旦有功能的加入、或者老人的离职

这个将会是灾难性的,对于新人的入手也会难上加难,所以凡事都有两面性,结合自身的业务需要去做技术选型,脱离业务谈技术就是耍流氓!

 

这个时候常见的做法就是把巨无霸拆,拆成多个系统,各个系统之间通过网络通讯来完成交互,就是原来的本地API调用,转变成了跨进程,跨服务器的远程调用;

这样子做一下子清晰明了,但是随之而来的就是系统之间错综复杂的调用,事务的一致性无法都到保证,原来只要维护很少的应用现在一下子多了N个。。

 

即使有上述很多问题,大家还是愿意这样去做,为啥?个人觉得就是现在有很多成熟的开源框架供人们去使用,接入成本大大降低;

想想看,如果没有这些开源的或者商用的技术服务,你敢去拆,一个服务的调用稳定、容灾、并发等等都是需要我们去考虑的等等,就需要大量的中间件去帮我们去屏蔽这些细节!

 

那么分布式系统出来了,下面就是随之而来的分布式锁、分布式事务、RPC、消息队列、全局ID等一系列高阶玩家需要去摸索和了解的;

 

  • 分布式锁

 我们平常见的比较多的可能就是lock、sychronized这些都是在一个进程中的多线程实现,一旦我们集群了这种加锁的方式就会失效,因为他不能跨JVM,

这个时候我们就需要借助第三方的东西帮我们实现功能

 

目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:

基于数据库实现

基于Redis实现

基于ZooKeeper实现

无论哪种方式,其实都不完美,依旧要根据咱们业务的实际场景来选择。

 

我们先来看一下如何基于「乐观锁」来实现:

乐观锁就是认为我在执行的时候乐观的以为其他人都不会执行,只有我在执行这更新操作;

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。

  当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,

且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

 

下面找图举例,

 

如图,假设同一个账户,用户A和用户B都要去进行取款操作,账户的原始余额是2000,用户A要去取1500,用户B要去取1000,

如果没有锁机制的话,在并发的情况下,可能会出现余额同时被扣1500和1000,导致最终余额的不正确甚至是负数。

但如果这里用到乐观锁机制,当两个用户去数据库中读取余额的时候,除了读取到2000余额以外,还读取了当前的版本号version=1,

等用户A或用户B去修改数据库余额的时候,无论谁先操作,都会将版本号加1,即version=2,

那么另外一个用户去更新的时候就发现版本号不对,已经变成2了,不是当初读出来时候的1,那么本次更新失败,就得重新去读取最新的数据库余额。

 

通过上面这个例子可以看出来,使用「乐观锁」机制,必须得满足:

 

(1)锁服务要有递增的版本号version

(2)每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

 

我们再来看一下如何基于「悲观锁」来实现:

悲观锁就是我在执行的时候,别人也会去执行,所以我必须把他锁起来,我再操作,不然我不做更新;

悲观锁也叫作排它锁,在Mysql中是基于 for update 来实现加锁的,例如:

 

//锁定的方法-伪代码

public boolean lock(){

    connection.setAutoCommit(false)

   for(){

  result =select * from user where id = 100 for update;

  if(result){

    //结果不为空,

    //则说明获取到了锁

    return true;

  }

  //没有获取到锁,继续获取

  sleep(1000);

  }

  return false;

}

//释放锁-伪代码

connection.commit();

 

上面的示例中,user表中,id是主键,通过 for update 操作,数据库在查询的时候就会给这条记录加上排它锁。

(需要注意的是,在InnoDB中只有字段加了索引的,才会是行级锁,否者是表级锁,所以这个id字段要加索引)

当这条记录加上排它锁之后,其它线程是无法操作这条记录的。

那么,这样的话,我们就可以认为获得了排它锁的这个线程是拥有了分布式锁,然后就可以执行我们想要做的业务逻辑,当逻辑完成之后,再调用上述释放锁的语句即可。

给大家看下如何去模拟这个排他锁,直接上截图,有图有真相;Mysql常用的客户端一般都是navicat;  

 

比如我们没有上锁,select * from t_shark_user t WHERE t.user_name='1111'(这种操作天生幂等),无论谁去查都OK,但是如果你加了 for update

变成select * from t_shark_user t WHERE t.user_name='1111' for update;这个就会有好玩的情境出现了;

首先,执行下面的命令看下你的自动提交时打开还是关闭的,默认是打开的,就是你每执行一个SQL,mysql会自动帮你commit,不需要你手动进行commit;

 

如果你的是自动提交的,你for update也没用,因为你刚锁完,他就帮你提交了,也就释放锁了;

show variables like 'autocommit';

为了模拟排他锁,执行命令  set autocommit = 0;这个时候Value就会变成OFF;

在其中一个窗口(session)中执行,怎么执行都是OK的,这条数据,因为这个会话已经获取了该条数据的行级锁,

但是新打开一个窗口,就会不一样了,不信继续往下看:一直在等待,当然他会有个超时时间;

 

 一直没有结果;select * from t_shark_user t WHERE t.user_name='张书' for update;

 但是查询另外一条数据是OK的,

 

利用redis实现分布式锁:

大家可以参考下面的博主分享的文章   https://www.lovecto.cn/20180814/203.html,亲测可以用;直接上代码

package com.cloudwalk.shark.config.redisson;

import com.cloudwalk.shark.config.inter.impl.RedissonLocker;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;


@Configuration
public class RedissonConfig {

    @Value("${mini.redis.host}")
    private String host;
    @Value("${mini.redis.port}")
    private String port;
    @Value("${mini.redis.passwd}")
    private String password;

    /**
     * RedissonClient,单机模式
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
        return Redisson.create(config);
    }

    @Bean
    public RedissonLocker redissonLocker(RedissonClient redissonClient){
        RedissonLocker locker = new RedissonLocker(redissonClient);
        //设置LockUtil的锁处理对象
        LockUtil.setLocker(locker);
        return locker;
    }
}

 

package com.cloudwalk.shark.config.redisson;

import com.cloudwalk.shark.config.inter.Locker;

import java.util.concurrent.TimeUnit;


/**
 * redis分布式锁工具类
 *
 */
public final class LockUtil {

    private static Locker locker;

    /**
     * 设置工具类使用的locker
     * @param locker
     */
    public static void setLocker(Locker locker) {
        LockUtil.locker = locker;
    }

    /**
     * 获取锁
     * @param lockKey
     */
    public static void lock(String lockKey) {
        locker.lock(lockKey);
    }

    /**
     * 释放锁
     * @param lockKey
     */
    public static void unlock(String lockKey) {
        locker.unlock(lockKey);
    }

    /**
     * 获取锁,超时释放
     * @param lockKey
     * @param timeout
     */
    public static void lock(String lockKey, int timeout) {
        locker.lock(lockKey, timeout);
    }

    /**
     * 获取锁,超时释放,指定时间单位
     * @param lockKey
     * @param unit
     * @param timeout
     */
    public static void lock(String lockKey, TimeUnit unit, int timeout) {
        locker.lock(lockKey, unit, timeout);
    }

    /**
     * 尝试获取锁,获取到立即返回true,获取失败立即返回false
     * @param lockKey
     * @return
     */
    public static boolean tryLock(String lockKey) {
        return locker.tryLock(lockKey);
    }

    /**
     * 尝试获取锁,在给定的waitTime时间内尝试,获取到返回true,获取失败返回false,获取到后再给定的leaseTime时间超时释放
     * @param lockKey
     * @param waitTime
     * @param leaseTime
     * @param unit
     * @return
     * @throws InterruptedException
     */
    public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
                                  TimeUnit unit) throws InterruptedException {
        return locker.tryLock(lockKey, waitTime, leaseTime, unit);
    }

    /**
     * 锁释放被任意一个线程持有
     * @param lockKey
     * @return
     */
    public static boolean isLocked(String lockKey) {
        return locker.isLocked(lockKey);
    }
}

 

package com.cloudwalk.shark.controller;

import com.cloudwalk.shark.config.redisson.LockUtil;
import com.cloudwalk.shark.model.User;
import com.cloudwalk.shark.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.concurrent.TimeUnit;

@Controller
@RequestMapping(value = "/lock", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
public class SharkDBLockController {
    @Autowired
    private UserService userService;
    @PostMapping("/db/{userName}")
    @ResponseBody
    public void updateUserScore(@PathVariable("userName") String userName) throws InterruptedException {
        if(LockUtil.tryLock("1", 1,5,TimeUnit.SECONDS) ){
            System.out.println("接收到请求"+userName);
            User user = userService.findUserByName(userName);
            Thread.sleep(2000);
            user.setScore(user.getScore() + 1);
            userService.updateUser(user);
        }else{
            System.out.println("指定时间内没有获取到相应的锁!");
        }
    }
}

通过调节,waitTime,以及leaseTime可以看到效果;

/**
* 尝试获取锁,在给定的waitTime时间内尝试,获取到返回true,获取失败返回false,获取到后再给定的leaseTime时间超时释放
* @param lockKey
* @param waitTime
* @param leaseTime
* @param unit
* @return
* @throws InterruptedException
*/
public static boolean tryLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit) throws InterruptedException {
return locker.tryLock(lockKey, waitTime, leaseTime, unit);
}

 

 

分布式事务

分布式锁

消息队列

RPC

全局唯一ID生成策略

posted @ 2019-05-28 15:13  龙X  阅读(217)  评论(0编辑  收藏  举报