服务器网络波动的坑

因为自己做防封系统,所以用户入口全部经过我的服务器,一天几十W IP 流量,但会出现网络连接和网络流量和数据库连接数彪起来,然后数据流量降下问题,我想了好久不知道问题在哪里。开始认为SQL 域名问题,后面发现这种情况出现的时候, 数据库根本就没有很多SQL语句执行,那么SQL语句就没有问题了。那么服务器代码写的有问题,哪里卡住导致,网络堆积起来,后面分析代码也没有问题。。。过几周,我在想是不是网络波动,因为为了免备案用的香港服务器,然后直接连接内地数据库。

因为以前别人都是用外部直接连接,没有内部连接。所以这个功能一直都是外部连接,当初我不做这个服务器开发, 我写完核心代码就没有管了。我后来直接香港服务器Nginx 代理内地服务器,这样子测试,测试一周基本非常稳定,这样就解决了备案的问题,又保证了服务器稳定性问题。

 

总结:

数据库数据传递比较多,一旦网络波动就数据库连接失败,或者数据传递慢,这样子导致服务器并发处理不过来。虽然香港服务器对内地服务器还是有波动,原因改成代理了,数据库数据不用传递到香港了,直接内网传递,速度不知道快了多少,服务器稳定提高了。

 

悲观锁,乐观锁,分布式锁(转载)

原作者地址:https://www.jianshu.com/p/f5ff017db62a
悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
乐观锁一般来说有以下2种方式:
  1. 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  2. 使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
Java JUC中的atomic包就是乐观锁的一种实现,AtomicInteger 通过CAS(Compare And Set)操作实现线程安全的自增。
MySQL隐式和显示锁定
MySQL InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据事务隔离级别在需要的时候自动加锁。
另外,InnoDB也支持通过特定的语句进行显示锁定,这些语句不属于SQL规范:
  • SELECT … LOCK IN SHARE MODE
  • SELECT … FOR UPDATE
实战
接下来,我们通过一个具体案例来进行分析:考虑电商系统中的下单流程,商品的库存量是固定的,如何保证商品数量不超卖? 其实需要保证数据一致性:某个人点击秒杀后系统中查出来的库存量和实际扣减库存时库存量的一致性就可以。
假设,MySQL数据库中商品库存表tb_product_stock 结构定义如下:
CREATE TABLE `tb_product_stock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘自增ID’, `product_id` bigint(32) NOT NULL COMMENT ‘商品ID’, `number` INT(8) NOT NULL DEFAULT 0 COMMENT ‘库存数量’, `create_time` DATETIME NOT NULL COMMENT ‘创建时间’, `modify_time` DATETIME NOT NULL COMMENT ‘更新时间’, PRIMARY KEY (`id`), UNIQUE KEY `index_pid` (`product_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=’商品库存表’;
对应的POJO类:
classProductStock {private Long productId; //商品idprivate Integer number; //库存量public Long getProductId(){ return productId; } publicvoidsetProductId(Long productId){ this.productId = productId; } public Integer getNumber(){ return number; } publicvoidsetNumber(Integer number){ this.number = number; }}
不考虑并发的情况下,更新库存代码如下:
/** * 更新库存(不考虑并发) * @param productId * @return */publicbooleanupdateStockRaw(Long productId){ ProductStock product = query(“SELECT * FROM tb_product_stock WHERE product_id=#{productId}”, productId); if (product.getNumber() > 0) { int updateCnt = update(“UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}”, productId); if(updateCnt > 0){ //更新库存成功returntrue; } } returnfalse; }
多线程并发情况下,会存在超卖的可能。
悲观锁
/** * 更新库存(使用悲观锁) * @param productId * @return */publicbooleanupdateStock(Long productId){ //先锁定商品库存记录 ProductStock product = query(“SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE”, productId); if (product.getNumber() > 0) { int updateCnt = update(“UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}”, productId); if(updateCnt > 0){ //更新库存成功returntrue; } } returnfalse; }
乐观锁
/** * 下单减库存 * @param productId * @return */publicbooleanupdateStock(Long productId){ int updateCnt = 0; while (updateCnt == 0) { ProductStock product = query(“SELECT * FROM tb_product_stock WHERE product_id=#{productId}”, productId); if (product.getNumber() > 0) { updateCnt = update(“UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}”, productId, product.getNumber()); if(updateCnt > 0){ //更新库存成功returntrue; } } else { //卖完啦returnfalse; } } returnfalse; }
使用乐观锁更新库存的时候不加锁,当提交更新时需要判断数据是否已经被修改(AND number=#{number}),只有在 number等于上一次查询到的number时 才提交更新。
** 注意** :UPDATE 语句的WHERE 条件字句上需要建索引
乐观锁与悲观锁的区别
乐观锁的思路一般是表中增加版本字段,更新时where语句中增加版本的判断,算是一种CAS(Compare And Swep)操作,商品库存场景中number起到了版本控制(相当于version)的作用( AND number=#{number})。
悲观锁之所以是悲观,在于他认为本次操作会发生并发冲突,所以一开始就对商品加上锁(SELECT … FOR UPDATE),然后就可以安心的做判断和更新,因为这时候不会有别人更新这条商品库存。
小结
这里我们通过 MySQL 乐观锁与悲观锁 解决并发更新库存的问题,当然还有其它解决方案,例如使用 分布式锁。目前常见分布式锁实现有两种:基于Redis和基于Zookeeper,基于这两种 业界也有开源的解决方案,例如 Redisson Distributed locks Apache Curator Shared Lock ,这里就不细说,网上Google 一下就有很多资料。

数据库事务隔离级别(转载)

原作者:https://www.zhihu.com/question/30272728

好多精彩回答

最近帮服务器写功能模块(自己主要写客户端核心功能模块),所以收集一些自己以前不是很了解知识进行记录。(貌似图片没有了,有需要看原作者的答案)

 


先借用前辈的一句话:数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,锁的应用最终导致不同事务的隔离级别。
隔离性分为四个级别:
1读未提交:(Read Uncommitted)
2读已提交(Read Committed) 大多数数据库默认的隔离级别
3可重复读(Repeatable-Read) mysql数据库所默认的级别
4序列化(serializable)
四个级别的具体实现和不同的请下面细读:
首先程序是可以并发执行的,同样,在MySQL中,一个表可以由两个或多个进程同时来读写数据,这是没有问题的。
比如,此时有两个进程来读数据,这也没什么问题,允许。但是如果一个进程在读某一行的数据的过程中,另一个在进程又往这一行里面写数据(改、删),那结果会是如何?同样,如果两个进程都同时对某一行数据进行更改,以谁的更改为准?那结果又会怎样,不敢想象,是不是数据就被破坏掉了。所以此时是冲突的。
既然会冲突就要想办法解决,靠谁来解决,这时候就是靠锁机制来维护了。怎么使用锁来使他们不冲突?
在事务开始的时候可以给要准备写操作的这一行数据加一个排它锁,如果是读操作,就给该行数据一个读锁。这样之后,在修改该行数据的时候,不让其他进程对该行数据有任何操作。而读该行数据的时候,其他进程不能更改,但可以读。读或写完成时,释放锁,最后commit提交。这时候读写就分离开了,写和写也就分离开了。
注意:此时加锁和释放锁的过程由mysql数据库自身来维护,不需要我们人为干涉。mysql开发者给这个解决冲突的方案起了一个名字叫做:读未提交:(Read Uncommitted)。这也就是事务的第一个隔离性。
但是这个程度的隔离性仅仅是不够的。看下面的测试结果:
1)A修改事务级别为:未提交读。并开始事务,对user表做一次查询
2)B事务更新一条记录
3)此时B事务还未提交,A在事务内做一次查询,发现查询结果已经改变
4)B进行事务回滚
5)A再做一次查询,查询结果又变回去了
由试验得知:在一个进程的事务当中,我更改了其中的一行数据,但是我修改完之后就释放了锁,这时候另一个进程读取了该数据,此时先前的事务是还未提交的,直到我回滚了数据,另一个进程读的数据就变成了无用的或者是错误的数据。我们通常把这种数据叫做脏数据,这种情况读出来的数据叫做賍读。
怎么办?依然是靠锁机制。无非是锁的位置不同而已,之前是只要操作完该数据就立马释放掉锁,现在是把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作。那么数据库为此种状态的数据库操作规则又给了一个名字叫做:读已提交(Read Committed),或者也可以叫不可重复读。这也就是事务的第二个隔离性。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
继续看下面的测试结果:
1)把隔离性调为READ-COMMITTED(读取提交内容)设置A的事务隔离级别,并进入事务做一次查询
2)B开始事务,并对记录进行修改
3)A再对user表进行查询,发现记录没有受到影响
4)B提交事务
5)A再对user表查询,发现记录被修改
试验进行到这里,你会发现,在同一个事务中如果两次读取相同的数据时,最后的结果却不一致。这里我们把这种现象称为:不可重复读。因为在第一个事务读取了数据之后,此时另一个事务把该数据给修改了,这时候事务提交,那么另一个事务在第二次读取的时候,结果就不一样,一个修改前的,一个是修改后的。
但是细心的你会发现,既然你说此种隔离性是在事务提交后才释放锁,那么在试验过程中,在该数据未提交前,另一个事务为什么也是仍然可以读取的呀。是我说错了吗?不是的,在这里mysql使用了一个并发版本控制机制,他们把它叫做MVCC,通俗的也就是说:mysql为了提高系统的并发量,在事务未提交前,虽然事务内操作的数据是锁定状态,但是另一个事务仍然可以读取,大多数数据库默认的就是这个级别的隔离性。但mysql不是。
而且不只是在更新数据时出现这个问题,在插入数据时仍然会造成类似的这样一种现象:mysql虽然锁住了正在操作的数据行,但它仍然不会阻止另一个事务往表插入新行新的数据。比如:一个事务读取或更新了表里的所有行,接者又有另一个事务往该表里插入一个新行,在事务提交后。原来读取或更改过数据的事务又第二次读取了相同的数据,这时候这个事务中两次读取的结果集行数就不一样。原来更新了所有行,而现在读出来发现竟然还有一行没有更新。这就是所谓的幻读。
为了防止同事务中两次读取数据不一致,(包括不可重读和幻读),接下来该如何继续做呢?!
mysql依然采取的是MVCC并发版本控制来解决这个问题。具体是:如果事务中存在多次读取同样的数据,MySQL第一次读的时候仍然会保持选择读最新提交事务的数据,当第一次之后,之后再读时,mysql会取第一次读取的数据作为结果。这样就保证了同一个事务多次读取数据时数据的一致性。这时候,mysql把这种解决方案叫做:可重复度(Repeatable-Read),也就是上述所写的第三个隔离性,也是mysql默认的隔离级别。
注意:幻读和不可重复读(Read Committed)都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
说到这里,真的就完事了吗?到这里其实mysql并未完全解决数据的一致性问题。只是在读取上做了手脚,解决了传统意义上的幻读和不可重复读。
例子:1 A事务开启,B事务开启。
2 B事务往表里面插入了一条数据,但还并未提交。
3 A事务开始查询了,并没有发现B事务这次插入的数据。然后此时B事务提交了数据。
4 于是乎,A事务就以为没有这条数据,就开始添加这条数据,但是却发现,发生了数据 重复冲突。
最后这个时候,该我们的最后一种隔离级别也是最高的隔离级:别序列化(serializable)登场了。
该隔离级别会自动在锁住你要操作的整个表的数据,如果另一个进程事务想要操作表里的任何数据就需要等待获得锁的进程操作完成释放锁。可避免脏读、不可重复读、幻读的发生。当然性能会下降很多,会导致很多的进程相互排队竞争锁。
后记:以上所说的四种隔离性的锁机制应用是数据库自动完成的,不需要人为干预。隔离级别的设置只对当前链接有效。对于使用MySQL命令窗口而言,一个窗口就相当于一个链接,当前窗口设置的隔离级别只对当前窗口中的事务有效
本文会继续更新,后面会探讨死锁,已经我们该如何对锁技术的应用该进行选型!