为您推荐:
精华内容
最热下载
问答
  • 5星
    5.34MB qing_gee 2021-08-03 16:12:27
  • 5星
    69.48MB qq_17556735 2021-03-13 22:42:11
  • 5星
    2KB qq_26403571 2021-06-06 18:18:34
  • 5星
    3.98MB haohao123nana 2020-11-12 11:45:33
  • 01、说一个基本的问题:高并发下的操作mysql数据产生的数据冲突问题 --并发控制:当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独...

    01、说一个基本的问题:高并发下的操作mysql数据产生的数据冲突问题

    --并发控制:当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。
    	--没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。
    	--常说的并发控制,一般都和数据库管理系统(DBMS)有关。在 DBMS 中的并发控制的任务,是确保在多个事务同时存取数据库中同一数据时,不破坏事务的隔离性、一致性和数据库的统一性
        --并发控制的主要手段大致可以分为乐观并发控制和悲观并发控制两种
        --无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像 hibernate、tair、memcache 等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进行对比。
        --乐观锁比较适用于读多写少的情况(多读场景),悲观锁比较适用于写多读少的情况(多写场景)。
    
    --通过上锁的方式解决高并发时的数据冲突:
    	--悲观锁:借助mysql锁机制实现,可能产生死锁
    		--悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】
    		--之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:
    			--传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
    			--Java 里面的同步 synchronized 关键字的实现
    		--悲观锁主要分为共享锁和排他锁:
    			--共享锁【shared locks】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
    			--排他锁【exclusive locks】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
    	
    		--[锁的级别]:需要注意一些锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意
    		
    		--表级锁
    			--表锁(MySQL layer):手动加
    				--read lock:加读锁后,可以加读锁不可以加写锁
    				--write lock:加写锁后,不可以加读锁也不可以加写锁
    			--元数据锁(MySQL layer):自动加
    				--CURD 加读锁
    				--DDL 加写锁
    			--意向锁(InnoDB):内部使用
    				--共享读锁(IS)
    				--排他写锁(IX)
    		--行级锁(InnoDB)
    			--共享读锁(S):手动加 -- select ... lock in share mode
    			--排他写锁(X):自动加 -- DML (insert, update, delete)
    							   -- select ... for update
    		--悲观锁适用场景:
    			▧ 无脏读  上锁数据保证一致, 因此无脏读, 对脏读不允许的环境悲观锁可以胜任
    			▧ 无并行  悲观锁对事务成功性可以保证, 但是会对数据加锁导致无法实现数据的并行处理
    			▧ 事务成功率高  上锁保证一次成功, 因此在对数据处理的成功率要求较高的时候更适合悲观锁
    			▧ 开销大  悲观锁的上锁解锁是有开销的, 如果超大的并发量这个开销就不容小视, 因此不适合在高并发环境中使用悲观锁 
    			▧ 一次性完成  如果乐观锁多次尝试的代价比较大,也建议使用悲观锁, 悲观锁保证一次成功
    
    	--乐观锁:通过程序实现,不会产生死锁
    		--乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。
    		--乐观锁一定要把mysql事务级别调整为 Read Committed[读取提交内容]:
    			--查看事务级别:select @@global.tx_isolation;
    			--RepeatableRead[可重读, Mysql默认的隔离级别]
    			--到mysql的配置文件中去修改transcation-isolation = READ-COMMITTED
    		--乐观锁的实现
    			--CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式
    			--版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功
    		--乐观锁适用场景:
    			▧ 脏读  乐观锁不涉及到上锁的处理, 因此在数据并行需求的时候是更适合乐观锁,当然会产生脏读, 不过用回滚取消掉了
    			▧ 高并发  相比起悲观锁的开销, 乐观锁也是比悲观锁更适合于高并发场景
    			▧ 事务成功率低  乐观锁不能保证每次事务的成功, 是使用回滚方式来保证数据一致性, 因此会导致事务成功率很低
    			▧ 读多写少  乐观锁适用于读多写少的应用场景,这样可以提高并发粒度
    			▧ 开销小  可能会导致很多次的回滚都不能拿到正确的处理回应, 因此如果对成功性要求低,而且每次开销小比较适合乐观锁
    
    --CAS介绍:乐观锁主要是 冲突检测 和 数据更新两个步骤,CAS就是Compare And Swap
    	--CAS 即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值(V)与预期原值(A)相匹配,那么处理器会自动将该位置值更新为新值(B)。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置(V)应该包含值(A)。如果包含该值,则将新值(B)放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。Java 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。java.util.concurrent包下大量的类都使用了这个 Unsafe.java 类的 CAS 操作。
    	--CAS存在问题:A B 两个线程同时取数据3,A线程通过操作将数据由3->2->3,B检查发现数据还是3因此进行了更新,这就可能会导致错误。因此对于需要并发控制的数据可以通过增加version号,每次操作+1来表示数据的新鲜度,当然也可以使用时间戳字段这种自带自增属性的方式。
    	--锁粒度控制:是指锁住资源的范围大小也可以等同于限制的严格程度,锁的范围越大 == 限制越严格。在高并发情况下,使用乐观锁就意味着一个线程成功就意味着其他线程失败。因此控制锁粒度非常重要,例如更新库存:
    		--update item set quantity = quantity - 1 where id = 1 and quantity -1 > 0  锁粒度较小
    		--update item set quantity = quantity - 1 where id = 1 and quantity = origin_quantity  锁粒度较大  
    		
    	--在高并发情况下,会出现大量线程空旋转,最好优化一下,这里按下不表
    
    --使用经验:
    	--如果重试代价比较大,就用悲观锁
    	--如果只是可能会发生冲突或者发生冲突时并发数其实不是非常高的话最好使用乐观锁,如果经常发生冲突且冲突时并发很高建议使用悲观锁,因为悲观锁不会导致自循环,减少线程空转
    	--乐观锁常用于多读  悲观锁常用于多写
    

    02、项目实践 – 验证高并发下数据会出问题

    --创建项目: django-admin startproject django_lock_pos
    --创建app: python manage.py startapp test_lock_pos
    --修改settings.py文件: 
    	--INSTALLED_APPS = [
            'django.contrib.admin',
            'django.contrib.auth',
            'django.contrib.contenttypes',
            'django.contrib.sessions',
            'django.contrib.messages',
            'django.contrib.staticfiles',
            'test_lock_pos'
        ]
    	--DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.mysql',
                'NAME': 'python',
                'USER': 'root',
                'PASSWORD': '123456liming',
                'HOST': '127.0.0.1',
                'PORT': '3306',
            }
        }
    	--LANGUAGE_CODE='zh-Hans'  #设置为中文模式
    	--TIME_ZONE='Asia/Shanghai'   #设置为中国时间
    	--ALLOWED_HOSTS = ['*']
    
    --增加一个model.py
    	from django.db import models
    
        # Create your models here.
        class GoodsInfo(models.Model):
            name = models.CharField(max_length=50, verbose_name='名称')
            stock = models.IntegerField(default=0, verbose_name='库存')
    
            class Meta:
                db_table = 'tb_goodsinfo'
                
    --运行  python manage.py makemigrations  为改动创建迁移记录
    --运行  python manage.py migrate  将操作同步到数据库
    
    --配置url
    	--总目录下配置:
    		from django.contrib import admin
    		from django.urls import path
    		from django.conf.urls import include, url
    
    		urlpatterns = [
    			path('admin/', admin.site.urls),
    			path('', include('test_Lock_Pos.urls', namespace='test_lock_pos'))
    		]
    	--分app配置:
    		from django.conf.urls import url
    		from django_lock_pos.test_lock_pos.views import *
    
    		urlpatterns = [
    			url(r'^goods/', Goods.as_view()),
    		]
    
    --view.py
    import time
    from django.shortcuts import render
    from django.http import HttpResponse
    from rest_framework.generics import GenericAPIView
    from .models import GoodsInfo
    # Create your views here.
    
    
    class Goods(GenericAPIView):
        """ 购买商品 """
    
        def post(self, request):
            # 获取请求头中查询字符串数据
            goods_id = request.GET.get('goods_id')
            count = int(request.GET.get('count'))
    
            # 查询商品对象
            goods = GoodsInfo.objects.filter(id=goods_id).first()
            # 获取原始库存
            origin_stock = goods.stock
            print(origin_stock)
    
            # 判断商品库存是否充足
            if origin_stock < count:
                return HttpResponse(content="商品库存不足", status=400)
    
            # 演示多个用户并发请求,因为postman不是并发的所以sleep一下
            time.sleep(5)
    
            # 减少商品的库存数量,保存到数据库
            goods.stock = origin_stock - count
            goods.save()
    
            return HttpResponse(content="操作成功", status=200)
    
    --postman使用:
    	--postman无法进行并发:
    		--通过sleep(5),将取数据和存数据之间加入5s睡眠,就可通过打开两个tab页,用手动方式点击模拟实现高并发时同时操作数据带来的数据误差问题
    

    03、项目实践 – 使用悲观锁解决高并发下的问题

    --借助mysql数据库自带的锁机制实现:
    	--from django.db import transaction
    		--01、直接使用将一段操作设置为事务
    			with transaction.atomic():
        			...
    		--02、装饰器方式 -- 下面代码所用就是装饰器方式
    			@transaction.atomic
    			def foo():
        			....
    
    import time
    from django.db import transaction
    from django.shortcuts import render
    from django.http import HttpResponse
    from rest_framework.generics import GenericAPIView
    from .models import GoodsInfo
    # Create your views here.
    
    
    class Goods(GenericAPIView):
        """ 购买商品 """
    
        @transaction.atomic
        def post(self, request):
            # 获取请求头中查询字符串数据
            goods_id = request.GET.get('goods_id')
            count = int(request.GET.get('count'))
    
            # 悲观锁查询
            goods = GoodsInfo.objects.select_for_update().filter(id=goods_id).first()
            # 获取原始库存
            origin_stock = goods.stock
            print(origin_stock)
    
            # 判断商品库存是否充足
            if origin_stock < count:
                return HttpResponse(content="商品库存不足", status=400)
    
            # 演示多个用户并发请求,因为postman不是并发的所以sleep一下
            time.sleep(5)
    
            # 减少商品的库存数量,保存到数据库
            goods.stock = origin_stock - count
            goods.save()
    
            return HttpResponse(content="操作成功", status=200)
    

    04、项目实践 – 使用乐观锁解决问题

    --这里通过比较要修改的字段stock是否与取出时的数据一致
    	--若一致,直接更新数据即可
    	--若不一致,重新取出stock数据进行更新
    
    import time
    from django.db import transaction
    from django.shortcuts import render
    from django.http import HttpResponse
    from rest_framework.generics import GenericAPIView
    from .models import GoodsInfo
    
    
    class Goods(GenericAPIView):
        """ 购买商品 """
    
        @transaction.atomic
        def post(self, request):
            # 获取请求头中查询字符串数据
            goods_id = request.GET.get('goods_id')
            count = int(request.GET.get('count'))
    
            while True:
                # 查询商品对象 -- 最基本查询
                goods = GoodsInfo.objects.filter(id=goods_id).first()
    
                # 获取原始库存
                origin_stock = goods.stock
                print(origin_stock)
    
                # 判断商品库存是否充足
                if origin_stock < count:
                    return HttpResponse(content="商品库存不足", status=400)
    
                # 演示多个用户并发请求,因为postman不是并发的所以sleep一下
                time.sleep(5)
    
                # 减少商品的库存数量,保存到数据库
                result = GoodsInfo.objects.filter(id=goods_id, stock=origin_stock).update(stock=origin_stock - count)
                if result == 0:
                    # 表示更新失败,有人抢先购买了商品,重新获取库存信息,判断库存
                    continue
                break
            return HttpResponse(content="操作成功", status=200)
    

    05、项目高并发模拟 – 代码模拟,不需要使用jmeter 和 postman + sleep() + 手动点击方式

    --pytest.ini
    [pytest]
    addopts = -s
    python_files = pyTest_*.py
    python_functions = test_*
    
    --pyTest_mysql_lock.py
    from gevent import monkey;monkey.patch_all()  # 猴子补丁
    import gevent
    import requests
    import threading
    import multiprocessing
    
    def sin_mysql_lock_pos():
        url = 'http://127.0.0.1:8000/goods/?goods_id=1&count=1'
        r = requests.post(url)
    
    # 多进程可以实现真正的并发测试 -- 注:for ... range(): requests.post(url, data) 是不能实现并发的
    # def test_mysql_lockpos_multiprocess():
    #     for i in range(10):
    #         p = multiprocessing.Process(target=sin_mysql_lock_pos)
    #         p.start()
    
    # 多线程之前以为由于gil锁会和for ... range()一样,但是也是可以实现并发的
    # def test_mysql_lockpos_multithread():
    #     for i in range(100):
    #         p = threading.Thread(target=sin_mysql_lock_pos)
    #         p.start()
    
    # 协程实现并发 -- 可以模拟,但是需要使用joinall,单独join是不行的
    def test_mysql_lockpos_multigevent():
        gevent_l = []
        for i in range(10):
            p = gevent.spawn(sin_mysql_lock_pos)
            gevent_l.append(p)
    
        gevent.joinall(gevent_l)
    

    03、mysql数据库的锁机制

    展开全文
    weixin_41097516 2021-01-31 19:19:15
  • 数据库事务是访问可能操作各种数据项的一个数据库操作序列,这些操作要么全部成功,要么...数据库事务和多线程一样,为了提高数据库处理事务的吞吐量,数据库也支持并发事务,在并发处理数据的过程中,也存在着安全...

    数据库事务是访问可能操作各种数据项的一个数据库操作序列,这些操作要么全部成功,要么全部失败。提起事务,大家都知道ACID属性,这些特性在前边的文章里都有详细的讲解,感兴趣的可以通过历史文章查看。在Java中有并发编程,可以多线程并发执行,并发可以提高程序执行的效率,也会带来线程安全的。数据库事务和多线程一样,为了提高数据库处理事务的吞吐量,数据库也支持并发事务,在并发处理数据的过程中,也存在着安全问题。

    我们本文将从并发事务可能引发的问题、解决并发问题、MySQL的锁机制、锁的实现等方面逐渐深入,探讨高并发场景下的事务调优问题。

    并发事务可能引发的问题

    1.数据丢失

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    2.脏读、

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    3.幻读

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    4.不可重复读

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    事务隔离解决的并发问题

    数据丢失可以基于数据库中的悲观锁来避免发生,即在查询时通过在事务中使用 select xx for update 语句来实现一个排他锁,保证在该事务结束之前其他事务无法更新该数据。我们也可以基于乐观锁来避免,即将某一字段作为版本号,如果更新时的版本号跟之前的版本一致,则更新,否则更新失败。剩下3 个问题,其实是数据库读一致性造成的,需要数据库提供一定的事务隔离机制来解决。

    MySQL 的锁机制

    InnoDB实现了两种类型的锁机制:共享锁(S)和排他锁(X)。共享锁允许一个事务读数据,不允许修改数据,如果其他事务要再对该行加锁,只能加共享锁;排他锁是修改数据时加的锁,可以读取和修改数据,一旦一个事务对该行数据加锁,其他事务将不能再对该数据加任务锁。

    不同的锁机制会产生不同的事务隔离级别,不同的隔离级别分别可以解决并发事务产生的问题,如读未提交、读已提交、可重复读、可序列化等。(1号发的《MySQL的事务隔离级别和长事务,看这一篇就够了》一文中有介绍过)

    InnoDB中的读已提交和可重复读隔离事务是基于多版本并发控制(MVCC)实现高性能事务。一旦数据被加上排他锁,其他的事务将无法加入共享锁,且处于阻塞等待状态,如果一张表有大量的请求,这样的性能将是无法支持的。

    MVCC对普通的Select 不加锁,如果读取的数据正在执行delete或者update操作,这时读取操作不会等待排他锁的释放,而是直接利用MVCC读取该行的数据快照。MVCC避免了对数据重复加锁的过程,大大提高了毒草在的性能。(数据快照是指在该行的之前版本的数据,而数据快照的版本是基于undo实现的,undo是用来做事务回滚的,记录了回滚的不同版本的行记录)

    锁的具体实现算法

    InnoDB既实现了行锁,也实现了表锁,行锁是通过索引实现的,如果不通过索引条件检索数据,那么InnoDB将表中所有的记录进行加锁,其实就是升级为表锁。

    行锁的具体实现算法有三种:record lock、gap lock和next-key lock。record lock是专门对索引项加锁;gap lock是对索引项之间的间隙加锁,next-key lock则是前面两种的组合,对索引项及其之间的间隙加锁。

    只在可重复读或以上隔离级别下的特定操作才会取得 gap lock 或 next-key lock,在 Select 、Update 和 Delete 时,除了基于唯一索引的查询之外,其他索引查询时都会获取 gap lock 或 next-key lock,即锁住其扫描的范围。

    优化高并发事务

    上边的讲解,都是为了对事务、锁和隔离级别更加深入了解,下边将聊聊高并发场景下的事务是如何调优的。

    结合业务场景,使用低级别事务隔离

    在高并发业务中,为了保证业务数据的一致性,操作数据库时往往会使用不同级别的事务隔离,隔离等级越高,并发性能就越低。

    那在实际的业务中,我们要如何选择呢,下边举两个例子:

    在修改用户的最后登录时间,或者用户的个人资料等数据时,这些数据都只有用户自己登录和登陆后才会修改,不存在一个事务提交的信息被覆盖的可能,所以这样的业务我们就最低的隔离级别。

    如果账户的余额或者积分的消费,就可能存在多个客户端同时消费一个账户的情况,此时我们应该选择可重复读隔离级别,来保证当一个客户端在操作的时候,其他客户端不能对该数据进行操作。

    避免行锁升级表锁

    我们知道,InnoDB中行锁是通过索引实现的,当不通过索引条件检索数据时,行锁就会升级成表锁,我们知道表锁会严重影响我们对整张表的操作,应该避免这种情况。

    控制事务的大小,减少锁定的资源和锁定的时间

    下边这个SQL异常相比很多并发比较高的系统里都会遇见,比如抢购系统的日志中:

    MySQLQueryInterruptedException: Query execution was interrupted

    由于抢购系统中,提交订单业务开启了事务,在并发环境中对一条记录进行更新操作的情况下,由于更新记录所在的事务还可能存在其他操作,导致一个事务比较长,当大量请求进入时,就可能导致一些请求同时进入事务中,由于锁的竞争是不公平的,当多个事务同时对一条记录进行更新时,极端情况下,一个更新操作进去排队系统后,可能会一直拿不到锁,最后因超市被系统中断,就会抛出上边这个异常。

    提交订单需要创建订单和扣减库存,两种不同顺序的执行方式,结果都一样,但是性能确实不一样的:

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    这两种不同的执行方式,虽然这些操作都在一个事务中,但是锁的申请不在同一时间,锁只有当其他操作都执行完成才会释放锁。扣减库存是更新操作,属于行锁,如果先扣减库存会影响到其他操作该数据的事务,所以我们应该尽可能的避免长时间持有该锁,尽快的释放锁。

    因为创建订单和扣除库存不管先执行哪一步都不影响业务,所以我们可以先执行新增操作,把扣除库存放到最后,也就是使用执行顺序1 ,来减少锁的持有时间。

    总结

    MySQL 的并发事务调优和 Java 的多线程编程调优非常类似,都是可以通过减小锁粒度和减少锁的持有时间进行调优。在 MySQL 的并发事务调优中,我们尽量在可以使用低事务隔离级别的业务场景中,避免使用高事务隔离级别。

    在功能业务开发时,我们往往会为了追求开发速度,习惯使用默认的参数设置来实现业务功能。例如,在 service 方法中,你可能习惯默认使用 transaction,很少再手动变更事务隔离级别。但要知道,transaction 默认是 RR 事务隔离级别,在某些业务场景下,可能并不合适。因此,我们还是要结合具体的业务场景,进行考虑。

    往期内容

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    如果对你有所帮助,不妨点个关注,每天都有干货内容分享

    68bfdf3476ef0c6acd06a67b23f67ce1.gif

    原文始发于微信公众号(故里学Java):高并发场景下的数据库事务调优

    展开全文
    weixin_36203080 2021-04-17 10:32:19
  • 1. 为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的? 面试题剖析 为...

    1. 为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?
    面试题剖析
    为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?)
    说白了,分库分表是两回事儿,大家可别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。

    我先给大家抛出来一个场景。

    假如我们现在是一个小创业公司(或者是一个 BAT 公司刚兴起的一个新部门),现在注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10。天,就这种系统,随便找一个有几年工作经验的,然后带几个刚培训出来的,随便干干都可以。

    结果没想到我们运气居然这么好,碰上个 CEO 带着我们走上了康庄大道,业务发展迅猛,过了几个月,注册用户数达到了 2000 万!每天活跃用户数 100 万!每天单表数据量 10 万条!高峰期每秒最大请求达到 1000!同时公司还顺带着融资了两轮,进账了几个亿人民币啊!公司估值达到了惊人的几亿美金!这是小独角兽的节奏!

    好吧,没事,现在大家感觉压力已经有点大了,为啥呢?因为每天多 10 万条数据,一个月就多 300 万条数据,现在咱们单表已经几百万数据了,马上就破千万了。但是勉强还能撑着。高峰期请求现在是 1000,咱们线上部署了几台机器,负载均衡搞了一下,数据库撑 1000QPS 也还凑合。但是大家现在开始感觉有点担心了,接下来咋整呢…

    再接下来几个月,我的天,CEO 太牛逼了,公司用户数已经达到 1 亿,公司继续融资几十亿人民币啊!公司估值达到了惊人的几十亿美金,成为了国内今年最牛逼的明星创业公司!天,我们太幸运了。

    但是我们同时也是不幸的,因为此时每天活跃用户数上千万,每天单表新增数据多达 50 万,目前一个表总数据量都已经达到了两三千万了!扛不住啊!数据库磁盘容量不断消耗掉!高峰期并发达到惊人的 5000~8000!别开玩笑了,哥。我跟你保证,你的系统支撑不到现在,已经挂掉了!

    好吧,所以你看到这里差不多就理解分库分表是怎么回事儿了,实际上这是跟着你的公司业务发展走的,你公司业务发展越好,用户就越多,数据量越大,请求量越大,那你单个数据库一定扛不住。

    分表
    比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。

    分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

    分库
    分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

    这就是所谓的分库分表,为啥要分库分表?你明白了吧。

    #    分库分表前    分库分表后
    并发支撑情况    MySQL 单机部署,扛不住高并发    MySQL从单机到多机,能承受的并发增加了多倍
    磁盘使用情况    MySQL 单机磁盘容量几乎撑满    拆分为多个库,数据库服务器磁盘使用率大大降低
    SQL 执行性能    单表数据量太大,SQL 越跑越慢    单表数据量减少,SQL 执行效率明显提升
    用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?
    这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。

    比较常见的包括:

    cobar
    TDDL
    atlas
    sharding-jdbc
    mycat
    cobar
    阿里 b2b 团队开发和开源的,属于 proxy 层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。

    TDDL
    淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。

    atlas
    360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。

    sharding-jdbc
    当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。

    mycat
    基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。

    总结
    综上,现在其实建议考量的,就是 sharding-jdbc 和 mycat,这两个都可以去考虑使用。

    sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 sharding-jdbc 的依赖;

    mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

    通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用 sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 mycat,然后大量项目直接透明使用即可。

    你们具体是如何对数据库如何进行垂直拆分或水平拆分的?
    水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

    垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

    这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。

    还有表层面的拆分,就是分表,将一个表变成 N 个表,就是让每个表的数据量控制在一定范围内,保证 SQL 的性能。否则单表数据量越大,SQL 性能就越差。一般是 200 万行左右,不要太多,但是也得看具体你怎么操作,也可能是 500 万,或者是 100 万。你的SQL越复杂,就最好让单表行数越少。

    好了,无论分库还是分表,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说 userid,自动路由到对应的库上去,然后再自动路由到对应的表里去。

    你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都ok了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。

    而且这儿还有两种分库分表的方式:

    一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
    或者是按照某个字段hash一下均匀分散,这个较为常用。
    range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

    hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。

    2. 现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?
    面试题剖析
    这个其实从 low 到高大上有好几种方案,我们都玩儿过,我都给你说一下。

    停机迁移方案
    我先给你说一个最 low 的方案,就是很简单,大家伙儿凌晨 12 点开始运维,网站或者 app 挂个公告,说 0 点到早上 6 点进行运维,无法访问。

    接着到 0 点停机,系统停掉,没有流量写入了,此时老的单库单表数据库静止了。然后你之前得写好一个导数的一次性工具,此时直接跑起来,然后将单库单表的数据哗哗哗读出来,写到分库分表里面去。

    导数完了之后,就 ok 了,修改系统的数据库连接配置啥的,包括可能代码和 SQL 也许有修改,那你就用最新的代码,然后直接启动连到新的分库分表上去。

    验证一下,ok了,完美,大家伸个懒腰,看看看凌晨 4 点钟的北京夜景,打个滴滴回家吧。

    但是这个方案比较 low,谁都能干,我们来看看高大上一点的方案。

    双写迁移方案
    这个是我们常用的一种迁移方案,比较靠谱一些,不用停机,不用看北京凌晨 4 点的风景。

    简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。

    然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据 gmt_modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。

    导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。

    接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。


    3. 如何设计可以动态扩容缩容的分库分表方案?
    考点分析
    对于分库分表来说,主要是面对以下问题:

    选择一个数据库中间件,调研、学习、测试;
    设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,比如 3 个库,每个库 4 个表;
    基于选择好的数据库中间件,以及在测试环境建立好的分库分表的环境,然后测试一下能否正常进行分库分表的读写;
    完成单库单表到分库分表的迁移,双写方案;
    线上系统开始基于分库分表对外提供服务;
    扩容了,扩容成 6 个库,每个库需要 12 个表,你怎么来增加更多库和表呢?
    这个是你必须面对的一个事儿,就是你已经弄好分库分表方案了,然后一堆库和表都建好了,基于分库分表中间件的代码开发啥的都好了,测试都 ok 了,数据能均匀分布到各个库和各个表里去,而且接着你还通过双写的方案咔嚓一下上了系统,已经直接基于分库分表方案在搞了。

    那么现在问题来了,你现在这些库和表又支撑不住了,要继续扩容咋办?这个可能就是说你的每个库的容量又快满了,或者是你的表数据量又太大了,也可能是你每个库的写并发太高了,你得继续扩容。

    这都是玩儿分库分表线上必须经历的事儿。

    面试题剖析
    停机扩容(不推荐)
    这个方案就跟停机迁移一样,步骤几乎一致,唯一的一点就是那个导数的工具,是把现有库表的数据抽出来慢慢倒入到新的库和表里去。但是最好别这么玩儿,有点不太靠谱,因为既然分库分表就说明数据量实在是太大了,可能多达几亿条,甚至几十亿,你这么玩儿,可能会出问题。

    从单库单表迁移到分库分表的时候,数据量并不是很大,单表最大也就两三千万。那么你写个工具,多弄几台机器并行跑,1小时数据就导完了。这没有问题。

    如果 3 个库 + 12 个表,跑了一段时间了,数据量都 1~2 亿了。光是导 2 亿数据,都要导个几个小时,6 点,刚刚导完数据,还要搞后续的修改配置,重启系统,测试验证,10 点才可以搞完。所以不能这么搞。

    优化后的方案
    一开始上来就是 32 个库,每个库 32 个表,那么总共是 1024 张表。

    我可以告诉各位同学,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。

    每个库正常承载的写入并发量是 1000,那么 32 个库就可以承载32 * 1000 = 32000 的写并发,如果每个库承载 1500 的写并发,32 * 1500 = 48000 的写并发,接近 5万/s 的写入并发,前面再加一个MQ,削峰,每秒写入 MQ 8 万条数据,每秒消费 5 万条数据。

    有些除非是国内排名非常靠前的这些公司,他们的最核心的系统的数据库,可能会出现几百台数据库的这么一个规模,128个库,256个库,512个库。

    1024 张表,假设每个表放 500 万数据,在 MySQL 里可以放 50 亿条数据。

    每秒的 5 万写并发,总共 50 亿条数据,对于国内大部分的互联网公司来说,其实一般来说都够了。

    谈分库分表的扩容,第一次分库分表,就一次性给他分个够,32 个库,1024 张表,可能对大部分的中小型互联网公司来说,已经可以支撑好几年了。

    一个实践是利用 32 * 32 来分库分表,即分为 32 个库,每个库里一个表分为 32 张表。一共就是 1024 张表。根据某个 id 先根据 32 取模路由到库,再根据 32 取模路由到库里的表。

    orderId    id % 32 (库)    id / 32 % 32 (表)
    259    3    8
    1189    5    5
    352    0    11
    4593    17    15
    刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个mysql服务器可能建了 n 个库,比如 32 个库。后面如果要拆分,就是不断在库和 mysql 服务器之间做迁移就可以了。然后系统配合改一下配置即可。

    比如说最多可以扩展到32个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到 1024 个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是1024个表。

    这么搞,是不用自己写代码做数据迁移的,都交给 dba 来搞好了,但是 dba 确实是需要做一些库表迁移的工作,但是总比你自己写代码,然后抽数据导数据来的效率高得多吧。

    哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。

    这里对步骤做一个总结:

    设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是 32库 * 32表,对于大部分公司来说,可能几年都够了。
    路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表
    扩容的时候,申请增加更多的数据库服务器,装好 mysql,呈倍数扩容,4 台服务器,扩到 8 台服务器,再到 16 台服务器。
    由 dba 负责将原先数据库服务器的库,迁移到新的数据库服务器上去,库迁移是有一些便捷的工具的。
    我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址。
    重新发布系统,上线,原先的路由规则变都不用变,直接可以基于 n 倍的数据库服务器的资源,继续进行线上系统的提供服务。
    4. 分库分表之后,id 主键如何处理?
    考点分析
    其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。

    面试题剖析
    基于数据库的实现方案
    数据库自增 id
    这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。

    这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增 id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,然后自己递增几个 id,一次性返回一批 id,然后再把当前最大 id 值修改成递增几个 id 之后的一个值;但是无论如何都是基于单个数据库。

    适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你并发不高,但是数据量太大导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

    设置数据库 sequence 或者表自增字段步长
    可以通过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。

    比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8。

    适合的场景:在用户防止产生的 ID 重复时,这种方案实现起来比较简单,也能达到性能目标。但是服务节点固定,步长也固定,将来如果还要增加服务节点,就不好搞了。

    UUID
    好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长了、占用空间大,作为主键性能太差了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。

    适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。

    UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf
    1
    获取系统当前时间
    这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。

    适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。

    snowflake 算法
    snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。

    1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
    41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2^41 - 1 个毫秒值,换算成年就是表示69年的时间。
    10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10台机器上哪,也就是1024台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2^5个机房(32个机房),每个机房里可以代表 2^5 个机器(32台机器)。
    12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 2^12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。
     

    0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000
    
    public class IdWorker {
    
        private long workerId;
        private long datacenterId;
        private long sequence;
    
        public IdWorker(long workerId, long datacenterId, long sequence) {
            // sanity check for workerId
            // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException(
                        String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(
                        String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            System.out.printf(
                    "worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                    timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
    
            this.workerId = workerId;
            this.datacenterId = datacenterId;
            this.sequence = sequence;
        }
    
        private long twepoch = 1288834974657L;
    
        private long workerIdBits = 5L;
        private long datacenterIdBits = 5L;
    
        // 这个是二进制运算,就是 5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
        private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    
        // 这个是一个意思,就是 5 bit最多只能有31个数字,机房id最多只能是32以内
        private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        private long sequenceBits = 12L;
    
        private long workerIdShift = sequenceBits;
        private long datacenterIdShift = sequenceBits + workerIdBits;
        private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        private long lastTimestamp = -1L;
    
        public long getWorkerId() {
            return workerId;
        }
    
        public long getDatacenterId() {
            return datacenterId;
        }
    
        public long getTimestamp() {
            return System.currentTimeMillis();
        }
    
        public synchronized long nextId() {
            // 这儿就是获取当前时间戳,单位是毫秒
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
                throw new RuntimeException(String.format(
                        "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }
    
            if (lastTimestamp == timestamp) {
                // 这个意思是说一个毫秒内最多只能有4096个数字
                // 无论你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }
    
            // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
            lastTimestamp = timestamp;
    
            // 这儿就是将时间戳左移,放到 41 bit那儿;
            // 将机房 id左移放到 5 bit那儿;
            // 将机器id左移放到5 bit那儿;将序号放最后12 bit;
            // 最后拼接起来成一个 64 bit的二进制数字,转换成 10 进制就是个 long 型
            return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
                    | (workerId << workerIdShift) | sequence;
        }
    
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        // ---------------测试---------------
        public static void main(String[] args) {
            IdWorker worker = new IdWorker(1, 1, 1);
            for (int i = 0; i < 30; i++) {
                System.out.println(worker.nextId());
            }
        }
    
    }
    
    

    怎么说呢,大概这个意思吧,就是说 41 bit 是当前毫秒单位的一个时间戳,就这意思;然后 5 bit 是你传递进来的一个机房 id(但是最大只能是 32 以内),另外 5 bit 是你传递进来的机器 id(但是最大只能是 32 以内),剩下的那个 12 bit序列号,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内。

    所以你自己利用这个工具类,自己搞一个服务,然后对每个机房的每个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是 0。然后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。

    利用这个 snowflake 算法,你可以开发自己公司的服务,甚至对于机房 id 和机器 id,反正给你预留了 5 bit + 5 bit,你换成别的有业务含义的东西也可以的。

    这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。
     

    展开全文
    zp19860529 2021-03-01 11:07:27
  • 在如此紧迫的环境下,时间光开发功能测试都不够,哪还有时间考虑整体系统架构,所以这个时候基本研发和leader也都达成一致,先上线后期再迭代优化。 这个时候基本就是单数据库的模式,什么是单数据库呢就是线上...

           想必在我们开发一个新项目或者一个新产品时候,都会遇到一个问题就是时间紧,任务重。为什么这么紧呢?有些可能是因为公司就是想着快速试错快速找准方向,有些可能就是非常迫切需要这么一个系统,当然也还有一些其他情况哈,就不多举例了。在如此紧迫的环境下,时间光开发功能测试都不够,哪还有时间考虑整体系统架构,所以这个时候基本研发和leader也都达成一致,先上线后期再迭代优化。

    图片

           这个时候基本就是单数据库的模式,什么是单数据库呢就是线上只有一个数据库,而且供多个数据库用户使用,这个模式往往是数据量不是很大。只需要满足基础的查询和写入操作就可,相对来说运维成本也很低。

    图片

           但是随着使用用户增多,数据量增大,查询需求变的越来越多。这个时候整体架构可以优化为用户查询请求先经过redis缓存,如果有直接从缓存中拿去数据返回,如果缓存中没有后再到数据库查询数据,查到数据后再缓存到redis中同时返回给用户。这种模式极大缓解的数据库的查询压力,但是这种模式也是极其危险的,因为一旦数据库挂掉,意味着你整个服务写能力将会不可用,读能力也会受到很大影响。尤其是这个阶段有一定的用户基础,一旦发生故障将是灾难性的影响。

    图片

           不慌,我们能想到的问题,早有先人给我们想好了,拿mysql来说,mysql支持一主多从架构。什么是一主多从呢,其实就是一个master机器,多个slave机器,master机器负责写入数据,同步数据给slave机器,也就是说slave机器和master机器将时刻保持数据同步,此时如果master查询压力过大我们可以把查询请求分配到slave机器上,分担master压力,提高并发处理能力。而且这种模式如果master机器挂掉后,运维可以手动操作让slave机器变为master机器继续提供服务,防止数据丢失和整个系统崩溃。那么我们现在已经是一主多从的稳定性架构那我们是不是就可以一劳永逸了呢,答案很明显是否定的,因为此种架构解决了查询压力大的问题,但是没有解决写入压力,写入还是集中在一个机器上,随着数据越来越多,单机写入会越来越不堪重负。

    图片

           那我们如何优化写入压力呢,这个可以从我们生活的一个例子举起。大家旅游的时候一定去过很多乡村,乡村公路大部分都是单车道,好一点的可能就是双车道,为什么它不搞成多车道呢?因为车少啊,那为啥省道和国道都是双向六车道、八车道,因为车多呀,哈哈哈。不好意思,有点跑偏了,我们的重点是学习这种思路,既然一个车道承载不了这么多车,那我们就多开几个呗。哎,你开窍了,数据库也是同理,一个写入压力大你就写入两个呗,两个不行就三个呗,分散承受,单个压力不就小了吗,假设一个数据库节点可以扛住5000并发,十个就5w并发,一百个就50w,天呐,想想就刺激。其实这种思路就是著名的分片思想。业界也有很多分片的中间件,比如说阿里的mycat、当当的sharding-jdbc、360的Atlas、小米的gaea等,感兴趣的小伙伴可以去了解下。空有思想但是我们也要落地呀,分散写是可以,但是业务数据怎么分配呢,比如说用户相关信息,聪明的小伙伴一定想到了可以按照带有数值的字段分呀,比如1-100w分到A库上,100.0001w-200w分到B库上这不就解决了吗,也就是我们常说的range模式。确实,可以解决一部分场景,但是有一些场景问题没有解决,比如存储用户订单信息,如果采用这种方式会有热点问题,比如说这种方式分配的数据最近几个月的数据都会集中在B库上,而且一个用户的数据会分散在多个库中,数据需要处理聚合。假设用户查询最近几个月数据占查询请求90%,这样90%压力都集中在B库上,在崩溃边缘疯狂试探,哪天上线新功能,你没有拜拜服务器,它给你一哆嗦你一年绩效就没有了。

    图片

           那这种场景如何解决呢,hash取模法,hash是一个很神奇的魔法,它可以近乎完美的让数据均匀分布,还是拿用户信息举例吧,如果用用户id做为hash函数输入,再除以你设定好的mysql master机器数量,就可以把不同用户数据分散均匀的打散到每个数据库上,而且各种用户信息也都会集中在一个库上存储,一个用户不同表操作再也不需要跨库了,再配合主从模式顺利解决读写瓶颈是不是收益多多呀,‘妈妈(老板)’再也不用担心数据库扛不住了。

    图片

          别急,再教你一招得八个月年终究极大法。光有存储分片还不够,后路得想好。后续业务极速发展,数据量大量上升,满了咋办,range方式扩容最简单,直接怼机器就行,无脑扩容。hash扩容可就复杂的多了,因为原来数据库存储读取的时候是按照hash取模方式路由的,直接加机器之前的规则就乱了,新增机器后大部分数据无法命中之前的数据库了。

    图片

           这里有几种比较公认的解决办法,一种是停机迁移,按照现有容量,计算好停服时间,提前做好迁移工具,事先发布停服通知,选择用户访问低谷时间段进行数据迁移。这种方式虽然最安全但是对用户体验伤害比较大,而且比较费时费力,稍有不慎就会导致核对时候数据对不上。

    图片

           第二种双写配置,也就是把新加入的机器加到线上环境中,跟现有机器组成集群,sharding组件进行数据的双写,应用逻辑的查询修改主要以老库为主,通过数据同步工具慢慢的把老数据从老库同步到新库中迁移后做数据库校验然后再修改sharding规则。

    图片

          第三种则是从库升级主库模式,也就是将原来主库下的从库解除主从关系,从库升级成新主库,由于主从模式使得从库和主库拥有同样的数据,所以升级上来的从库,只需要调整sharding规则分配新数据到新主库上即可,至于冗余数据,可以通过脚本慢慢删除即可,不影响提供服务,然后再将之前缺失的从库以及新主库的从库挂载上即可。这种方式操作较为简单,但是扩容的时候只能倍数扩容。

    图片

           当然分库分表也还有很多弊端比如事务一致性怎么保证,跨节点join咋整等等很多,就不在这里讨论了,有兴趣的同学可以评论区讨论。以上就是对于传统数据库高并发的一些实践经验,当然想要拿八个月年终,需要全方面了解,比如像业界内以TiDB为代表的新的大数据分布式存储解决方式newsql数据库,天生支持分布式存储、还兼容mysql、支持弹性扩展、分布式事务等,感兴趣可以去官网了解其架构原理。

    更多精彩内容请关注公众号:面向高薪编程

    展开全文
    mufei4886 2021-08-01 15:48:02
  • weixin_42462763 2021-02-06 21:00:35
  • qq_36341209 2021-09-07 23:00:35
  • xzl888888 2021-01-05 15:53:16
  • weixin_29062963 2021-03-06 19:24:36
  • weixin_32247165 2021-03-09 16:56:57
  • g6U8W7p06dCO99fQ3 2021-01-14 11:23:00
  • weixin_40990818 2021-02-05 21:20:45
  • weixin_49490052 2021-03-25 08:32:48
  • weixin_34060561 2021-01-19 06:19:14
  • yeidc_net 2021-03-25 02:39:02
  • weixin_51486343 2021-03-08 21:08:56
  • weixin_34902728 2021-03-18 12:36:11
  • y449739776 2021-11-28 19:06:48
  • weixin_31140863 2021-02-26 15:52:10
  • weixin_44271402 2021-04-26 10:54:58
  • weixin_39566914 2021-02-02 01:27:02
  • huangjinjin520 2021-09-02 01:14:14
  • EDDYCJY 2021-10-29 00:48:31
  • rakish_wind 2021-05-12 11:21:08
  • weixin_39679468 2021-01-19 02:19:58
  • weixin_34493012 2021-02-12 09:01:25
  • weixin_33289151 2021-01-19 23:58:59

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 123,919
精华内容 49,567
关键字:

高并发环境数据库优化