说说自己的理解,结合背景、业务、代码谈技术
- 分布式系统
结合自己之前做过的业务系统,有这样一个业务场景,也是比较常见的,销售、代理商需要登录系统去给商户开通相应的产品功能;
这个时候比较容易想到的几个业务场景,提单、拆单、工单审批、审批的过程中还需要一系列的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生成策略