vue h5 video 多视频无缝切换

思路

  1. 页面中创建两个 video 标签
  2. 在组件加载时同时加载两个视频资源(autoplay),暂时不播放的视频在 canplay 事件中调用暂停方法,使之实现预加载
  3. 使用绝对定位将预加载的视频移出可视窗口
  4. 第一个视频结束后,通过 ended 事件播放之前预加载的视频,同时将当前 src 切换为下一个需要播放的视频地址,完成缓存

核心代码

    <video ref="video" :class="{'video-hide':!playVideoTag}" autoplay @ended="onEnded('video')"></video>
    <video ref="video1" :class="{'video-hide':playVideoTag}" autoplay @ended="onEnded('video1')"></video>
  data() {
    return {
      srcList: [],
      curIndex: 0,
      playVideoTag: true, // 当前播放的 ref 是否为 video
      canPlayNext: true   // 是否存在下一个播放地址
    }
  },
    onEnded(e) {
      if (this.canPlayNext) {
        const video = e === 'video' ? this.$refs.video1 : this.$refs.video
        video.play()
        this.playVideoTag = !this.playVideoTag
        this.loadNext()
      }
      console.log('onended my video')
      this.$emit('ended')
    },
    playVideo(src) {  // 可以接受单个 url 和 url 列表
      this.srcList = typeof src === 'string' ? [src] : src
      this.$refs.video.src = src[0]
      this.srcList = src
      this.loadNext()
    },
    loadNext() {
      const video = this.playVideoTag ? this.$refs.video1 : this.$refs.video
      if (this.srcList.length - 1 === this.curIndex) { // 当前索引是列表的最后一个
        console.log('没有新视频')
        this.canPlayNext = false
        return
      }
      video.src = this.srcList[++this.curIndex]
	  video.addEventListener('canplay', e => e.target.pause(), { once: true })
    }
	.video-hide{
	  position: absolute;
	  top: -10000px;
	  left: -10000px;
	}

uni-app 横屏设置

打开APP就是横屏需要在app.vue中进行设置。

<script>    export default {        onLaunch: function() {            console.log('App Launch');			plus.screen.lockOrientation('landscape-primary'); //锁定        },        onShow: function() {            console.log('App Show');        },        onHide: function() {            console.log('App Hide');        }    }</script>

主要代码:plus.screen.lockOrientation(‘landscape-primary’); 

API解释如下:

锁定屏幕方向

void plus.screen.lockOrientation( String orientation );

说明:

锁定屏幕方向后屏幕只能按锁定的屏幕方向显示,关闭当前页面后仍然有效。 可再次调用此方法修改屏幕锁定方向或调用unlockOrientation()方法恢复到应用的默认值。

参数:

  • orientation: ( String ) 必选 要锁定的屏幕方向值
    锁定屏幕方向可取以下值: “portrait-primary”: 竖屏正方向; “portrait-secondary”: 竖屏反方向,屏幕正方向按顺时针旋转180°; “landscape-primary”: 横屏正方向,屏幕正方向按顺时针旋转90°; “landscape-secondary”: 横屏方向,屏幕正方向按顺时针旋转270°; “portrait”: 竖屏正方向或反方向,根据设备重力感应器自动调整; “landscape”: 横屏正方向或反方向,根据设备重力感应器自动调整;

返回值:

void : 无

uni-app 数据缓存

1.uni.setStorage(OBJECT) 与 uni.getStorage(OBJECT)    这两个是异步缓存,简单说就是将数据放到本地缓存指定的key中,一个存一个取罢了

    uni.setStorage(OBJECT) :将数据缓存在本地缓存中指定的key中,会覆盖掉原来该key中的内容,这是一个异步接口。(

参数名      类型         是否必填    说明

key        String        是        本地缓存中的指定的 key

data       Object/String   是        需要存储的内容

success     Function       否       接口调用成功的回调函数

fail       Function       否       接口调用失败的回调函数

complete    Function      否        接口调用结束的回调函数(调用成功、失败都会执行)

uni.getStorage(OBJECT) : 从本地缓存中异步获取指定key对应的内容(

参数名       类型       是否必填     说明

 key          String      是         本地缓存中的指定的key

 success       Function     是         接口调用的回调函数res = {data: key对应的内容}

fail         Function     否         接口调用失败的回调函数

complete       Function     否         接口调用结束的回调函数(调用成功、失败都会执行)

     那个例子跟大伙说一下,最近公司做项目,涉及到了一个存放地址的, 将地址的信息缓存到本地指定的URL 这个key中 进行存数据与取数据的操作,废话不多说,上代码~~

 下一步进行将数据缓存在本地

 下一步读取缓存在本地数据

//这是获取key中的内容 {data: “/pages/newHouse/NewHouseDetail?id=41”, errMsg: “getStorage:ok”}

  绿色标记为我在本地缓存中获取到的数据,res.data 就是我们需要的url地址了,如果需要直接获取就可以了,这一步骤就给大家展示了uni.setStorage(OBJECT) 与 uni.getStorage(OBJECT)  整体过程

2.uni.setStorageStnc(KEY,DATA) 与 uni.getStorage(KEY)    其实这个跟第一个基本上是没有区别的,只不过是一个同步一个异步罢了,参数说明及写法展示给大家

  uni.setStorageStnc(KEY,DATA)     将data存储在本地缓存中个指定的key中,会覆盖相同key中对应的内容,这是一个同步接口

        参数名      类型         是否必填    说明

        key        String        是        本地缓存中的指定的 key

                            data       Any   是        需要存储的内容,只支持原生类型、及能够通过 JSON.stringify 序列化的对象

    还是以地址url为例,以上个例子为基础来说  

uni.getStorageStnc(KEY)     从本地缓存中同步获取指定 key 对应的内容。

        参数名      类型         是否必填    说明

        key        String        是        本地缓存中的指定的 key

获取同步的缓存key中地址信息         

3.uni.removeStorage(OBJECT) 与 uni.removeStorageSync(KEY)   这两个都是从本地缓存中移除指定的key   与上面的也是一样样样的~~~   前面是异步的后面是同步的     异步为例~~~~

这是我打印出来的数据   通过了一个点击事件,这是显示我移除成功了,然后的~~~~等等等  等你在点击的时候会发现他并不打印了   也许你会因此疑惑  那是因为我已经将这个key所对应的数据移除了呀,所以当然是什么都不打印的啦~~~

4.uni.clearStorage() 与 uni.clearStorageSync()   这两个都是清理本地数据的缓存    当然啦  还是一个异步一个同步的    这个跟上一个其实是一样的   就是说这个吧 emmmmm   清除了你所有的本地数据   上一个只是清除了你本地指定key中的内容

写法就是直接emmmm 写!!!    uni.clearStorage()和uni.clearStorageSync()

作者:家乡_a6ce
链接:https://www.jianshu.com/p/cef270f827fa
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

MySQL异常ERROR 1055 (420ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause

大致错误如:ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘aaa.test.age’ which is not functionally dependent on columns in GROUP BY clause;
this is incompatible with sql_mode=only_full_group_by

一般是执行类似如下语句报错:

mysql> select * from customers group by age;

解决方法:

1、登陆mysql服务器,执行以下两条命令,在global与session级都修改;

mysql> set global sql_mode=‘STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION’;

mysql> set session sql_mode=‘STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION’;

2、同时,在my.cnf文件的[mysqld]字段中,指定sql_mode的值:

[mysqld]
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

以上两种方法同时执行后,不用重启mysql

——————————————————————————————————
注:我只修改了第1步即可成功执行了
原文链接:https://www.cnblogs.com/weiyiming007/p/10025181.html

发一个WebSocket+PHP聊天室-PHP多进程实时服务器推送技术

这一阵子在家写了个PHP聊天室,给大家献上代码,有用到的可以拿去用。

先介绍下聊天室的特性:
1、服务端采用纯php开发,不依赖php-fpm、nginx、或者apache数据库等,部署简单
2、采用php多进程,充分利用cpu资源
3、gateway workers进程模型,gateway进程只负责网络IO,worker进程负责业务逻辑,各尽其责,稳定高效
4、支持libevent事件轮询库,支持Epoll高并发(理论上可以支持上万的不活跃连接),服务端实时推送
5、支持分布式部署,可横向扩容
6、使用Websocket协议,占用带宽小,性能更好
7、客户端跨浏览器支持,只要浏览器支持flash或者支持websocket即可使用

demo点击这里(http://workerman.net:55151/)

安装、启动、使用就三步(不支持Win系统):
1、下载代码 ,并解压到任意目录

2、启动服务 ./bin/workermand start 如图:

3、访问启动服务的服务器55151端口 例如:http://127.0.0.1:55151 如图:

最后说明:
        这个聊天室是基于workerman框架开发的,workerman不支持winows系统,所以这个聊天室服务端不能在windows上部署。
        这个聊天室只是一个简单的demo,业务逻辑+注释不到200行代码(applications/Chat/Event.php中),大家可以根据自己的需要加上分组、私聊、表情等功能。

最后再发一下PHP聊天室下载地址 workerman-chat
————————————————
版权声明:本文为CSDN博主「walkor」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/udefined/article/details/23177119

使用Linux服务器搭建直播服务器,并运用Apicloud平台简易快速的制作全功能直播APP

simpleLive

使用Linux服务器搭建直播服务器,并运用Apicloud平台简易快速的制作全功能直播APP。

代码目录结构介绍

widget文件夹里是源代码,熟悉Apicloud的朋友对这个文件夹都不陌生,这里面包括所用到的aliyunLive模块代码。具体其目录结构和代码结构需使用者自行研究。

app:我作为开发者所关注并开发的部分,我编写的代码都在这里面。注意,这个目录不包含aliyunLive模块。

服务器端

打开相关端口

Rtmp会使用到1935端口

  1. 到阿里云后台设置本实例安全组规则开放1935端口。
  2. 配置防火墙打开1935端口。

安装FFMPEG

  1. 安装依赖包和升级相关工具

yum -y install gcc glibc glibc-devel make nasm pkgconfig lib-devel openssl-devel expat-devel gettext-devel libtool mhash.x86_64 perl-Digest-SHA1.x86_64

yum -y update gcc

yum -y install gcc+ gcc-c++

  1. 安装相关工具包
  2. git

yum install -y git

  • zlib

yum install -y zlib

  • pcre

yum install -y pcre

  • yadmi

cd /

mkdir tommycd tommy

wget http://sourceforge.net/projects/yamdi/files/yamdi/1.4/yamdi-1.4.tar.gz/download

tar -zxvf downloadcd yamdi-1.4

make && make installcd ..

  • openssl

yum install -y openssl

  1. 安装ffmpeg及其依赖包
  2. Yasm

wget http://www.tortall.net/projects/yasm/releases/yasm-1.2.0.tar.gz

tar -zxvf yasm-1.2.0.tar.gz

cd yasm-1.2.0

./configuremake && make install

cd ..

  • x264

git clone git://git.videolan.org/x264

cd x264

./configure –enable-sharedmake && make install

cd ..

  • LAME

wget http://downloads.sourceforge.net/project/lame/lame/3.99/lame-3.99.5.tar.gz

tar -zxvf lame-3.99.5.tar.gz

cd lame-3.99.5

./configure –enable-nasmmake && make install

cd ..

  • libogg

wget http://downloads.xiph.org/releases/ogg/libogg-1.3.0.tar.gz

tar -zxvf libogg-1.3.0.tar.gz

cd libogg-1.3.0

./configuremake && make install

cd ..

  • libvorbis

安装这个工具时废了很大力气,有报错,后来重复确认安装libogg后才成功。

wget http://downloads.xiph.org/releases/vorbis/libvorbis-1.3.3.tar.gz

tar -zxvf libvorbis-1.3.3.tar.gz

cd libvorbis-1.3.3

./configuremake && make install

cd ..

  • libvpx

git clone http://git.chromium.org/webm/libvpx.git

cd libvpx

./configure  –enable-sharedmake && make install

cd ..

  • faad2

wget http://downloads.sourceforge.net/project/faac/faad2-src/faad2-2.7/faad2-2.7.tar.gz

tar -zxvf faad2-2.7.tar.gz

cd faad2-2.7

./configuremake && make install

cd ..

  • fdk-acc

新版本的FFMPEG已经取消对faac的支持,改用fdk-acc进行解码,效率会更高。

wget https://nchc.dl.sourceforge.net/project/opencore-amr/fdk-aac/fdk-aac-0.1.5.tar.gz

tar -zxvf fdk-aac-0.1.5.tar.gz

cd fdk-aac-0.1.5

./configuremake && make install

cd ..

  • Xvid

wget http://downloads.xvid.org/downloads/xvidcore-1.3.2.tar.gz

tar zxvf xvidcore-1.3.2.tar.gz

cd xvidcore/build/generic

./configuremake && make install

cd ..

  • ffmpeg

git clone https://github.com/FFmpeg/FFmpeg.gitcd ffmpeg

./configure  –prefix=/opt/ffmpeg/ –enable-version3 –enable-libvpx –enable-libfdk-aac –enable-libmp3lame –enable-libvorbis –enable-libx264 –enable-libxvid –enable-shared –enable-gpl –enable-postproc –enable-nonfree  –enable-avfilter –enable-pthreadscd ..

  • 修改/etc/ld.so.conf如下:

include ld.so.conf.d/*.conf

/lib

/lib64

/usr/lib

/usr/lib64

/usr/local/lib

/usr/local/lib64

/opt/ffmpeg/lib

然后在/etc目录执行相关命令

cd /etc

ldconfig

  • 测试ffmpeg

我上传了一个test.mp4到/tommy下,使用命令查看ffmpeg运行情况

ffmpeg -re -i /tommy/test.mp4 -vcodec libx264 -acodec aac -f flv rtmp://my-ip:1935/live/steam

在终端看到以下命令则证明推流成功:

然后使用VCL(MAC端的一个播放器)等能播放rtmp流视频的播放器查看真正的结果是否成功:

查看播放结果:

如果上述都能如图显示,则证明FFMPEG安装和配置成功!

为Nginx安装nginx-rtmp-module

  1. 下载nginx-rtmp-module

cd /tommy

git clone https://github.com/arut/nginx-rtmp-module.git

  1. 动态添加nginx-rtmp-module模块

wget http://nginx.org/download/nginx-1.10.2.tar.gz

tar -zxvf nginx-1.10.2.tar.gz

cd nginx-1.10.2

./configure –prefix=/usr/share/nginx –sbin-path=/usr/sbin/nginx –modules-path=/usr/lib64/nginx/modules –conf-path=/etc/nginx/nginx.conf –error-log-path=/var/log/nginx/error.log –http-log-path=/var/log/nginx/access.log –http-client-body-temp-path=/var/lib/nginx/tmp/client_body –http-proxy-temp-path=/var/lib/nginx/tmp/proxy –http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi –http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi –http-scgi-temp-path=/var/lib/nginx/tmp/scgi –pid-path=/run/nginx.pid –lock-path=/run/lock/subsys/nginx –user=nginx –group=nginx –with-file-aio –with-ipv6 –with-http_ssl_module –with-http_v2_module –with-http_realip_module –with-http_addition_module –with-http_sub_module –with-http_dav_module –with-http_flv_module –with-http_mp4_module –with-http_gunzip_module –with-http_gzip_static_module –with-http_random_index_module –with-http_secure_link_module –with-http_degradation_module –with-http_slice_module –with-http_stub_status_module –with-mail_ssl_module –with-pcre –with-pcre-jit –with-stream=dynamic –with-stream_ssl_module –with-debug –add-module=/tommy/nginx-rtmp-module

cp /usr/sbin/nginx /usr/sbin/nginx.bak

cp ./objs/nginx /usr/sbin//usr/sbin/nginx -s reload

  1. 配置nginx.conf

vi /etc/nginx/nginx.conf

在server里添加

rtmp {

       server {

               listen 1935;

               application live {

                       live on;

               }

       }

}

重启nginx

/usr/sbin/nginx -s reload

APP端

将主播端和播放端集成到了同一个APP中,演示时请用两部安卓手机安装,一个做主播,一个负责播放。

概述

  1. 关于使用的平台
http://www.apicloud.com/
  1. 关于使用的模块

aliyunLive 封装了阿里云视频直播的 sdk,该模块包括视频流采集和视频流播放两部分。如使用阿里云直播服务器搭配此模块,需要在阿里云官网注册,在控制台创建直播流,并获取播放端相关参数,详情可以参照阿里云直播官网:阿里云视频直播。

注意: 该模块android版 播放器只适用于android 4.4+,推流需要 android 4.5 以上版本支持。iOS版最低版本为8.0。(iOS云编译的时候需要勾选相机和麦克风权限,并在高级设置里选择iOS版本为8.0)。

  1. 技术部分

  HTML5

  CSS3

  JS(Vue)

  1. 工具
  2. 编辑器Atom
  3. 直播模块aliyunLive,模块文档http://docs.apicloud.com/Client-API/Open-SDK/aliyunLive

主播部分

下方的地址最后一个参数steam可以随意更换,不同的地址可以视作不同的房间,并且观看端也配置相同的地址才能观看该房间的直播画面。

无论通过下方更换地址还是帧率等,都需要重新 配置直播流 并推流才能生效。

请依次点击“配置直播流”、“开始推流”进行直播:

播放部分

播放前必须先进行初始化,并点击准备按钮。

不同的地址可以视作不同的房间,只有和主播端相同的地址才能观看该房间的直播画面。

无论通过下方更换地址等,都需要重新 初始化->准备 才能生效。

Linux自建直播服务器一:使用Nginx+rtmp模块搭建流媒体服务器

2018-06-25 分类:服务器建站 阅读(6596) 评论(0)

技术服务于生活。当在日常遇到问题的时候,需要经常去思考如何解决这个问题。

最近网易的逆水寒开测了,非常荣幸拿到一个资格号,可以来体验一下这款国产巅峰级网游,一个会呼吸的江湖。作为一个极度业余的主播(一般只直播给朋友看),自然打开了斗鱼直播助手,准备直播一下。

可是比较无奈的是这款游戏的配置要求太高了,不开直播不卡,但是一开直播马上卡的起飞。一看斗鱼直播助手的cpu占用率飙到了30%。关闭这个软件,马上就好了。

后来想了一个解决方法,用OBS软件直接推流。OBS比较给力,一直占用10%左右的CPU。几乎没有任何卡感。OBS真是一个很强大的推流软件(关于OBS在后续文章会介绍)。后面的流媒体直播服务器,就是因为OBS而起。

闲话少说,下面进入正题吧。

既然有了推流软件,那么我们可以自己搭建一个私人的RTMP流媒体服务器,把屏幕录像转化成视频流推送到流媒体服务器,然后其他用户访问流媒体服务器就能看到直播了。用户不仅可以通过网页访问流媒体服务器,还可以通过播放器(potplayer、VLC)、APP等访问。流程如下图:

直播效果图(纯洁脸):

既然是直播服务器,那么大带宽、大流量是肯定的。如果只是偶尔私人直播,给几个朋友看看,那一般国外的云服务器也可满足要求(后续会更新本地搭建直播服务器,利用宽带直播)。但是要注意的是,你本地电脑到国外服务器如果延迟大的话,意味着丢包率也高。丢包率高了别人看你直播就会比较卡。如果使用国内云服务器,1M带宽….不用想了。所以这里我还是推荐香港的云服务器,到内地还是很稳定的。这篇博客就是以阿里云香港服务器30M带宽,1000G月流量为例。

环境:centos7系统,阿里云香港服务器

一、安装Nginx

流媒体服务器只需要Nginx+Nginx的rtmp模块就行了。首先ssh连接到服务器,通过LNMP一键安装包单独安装Nginx:

yum -y install wget

cd /root

wget -c http://soft.vpser.net/lnmp/lnmp1.5.tar.gz && tar zxf lnmp1.5.tar.gz

cd lnmp1.5./install.sh nginx

按任意键开始安装。等待安装完成后进入下一步。如果安装完成提示失败那么重新安装一次就行了

二、安装rtmp模块

  1. 下载nginx-rtmp-module:

yum -y install git

cd /root/lnmp1.5

git clone https://github.com/arut/nginx-rtmp-module.git

完成后在lnmp1.5目录下可以看到这个文件夹了:

  1. 使用如下命令查看编译安装参数:

nginx -V

可以看到:

在configure arguments后面就是安装参数了。把这个参数复制下来

  1. 重新编译安装nginx,在参数后面添加模块:

cd /root/lnmp1.5/src

tar zxvf nginx-1.14.0.tar.gz

cd nginx-1.14.0./configure –user=www –group=www –prefix=/usr/local/nginx –with-http_stub_status_module –with-http_ssl_module –with-http_v2_module –with-http_gzip_static_module –with-http_sub_module –with-stream –with-stream_ssl_module –with-openssl=/root/lnmp1.5/src/openssl-1.0.2o –add-module=../nginx-rtmp-module

make && make install

装完之后nginx就支持rtmp流媒体了。

三、配置推流地址以及播放页面

  1. 配置推流地址:

vim /usr/local/nginx/nginx.conf

如图,在events和http之间加入如图内容:

rtmp {

    server{

        listen 6666; #推流端口 可自定义

        application rtmplive{

             live on;

            max_connections 5;  #最大观看人数

         }

         application hls{

             live on;

            hls on;

            #wait_key on;

             hls_path /www/rtmp/hls;  #录像文件存储位置

             #hls_playlist_length 60s;

            hls_fragment 1s;  #视频切片大小

        }

     }

 }

  1. 配置播放页面

vim /usr/local/nginx/conf/vhost/你的域名.conf

加入如下内容:

  server {

       listen       80;

       server_name  你的域名;

       #charset koi8-r;

       #access_log  logs/host.access.log  main;

       location / {

          root   /www/rtmp;  #网站目录

          index  index.html index.htm;

      }

     #error_page  404              /404.html;

     # redirect server error pages to the static page /50x.html      #     error_page   500 502 503 504  /50x.html;      location = /50x.html {      root   html;     }  }

到这一步流媒体服务器就搭建完成了。下面再送一个web直播页面,很好用,强烈推荐:http://www.cuplayer.com/player/PlayerCodeCourse/2014/12171698.html

后面的以后再说

手把手教你搭建一个直播服务器(Nginx+Rtsp)

IOT_SHUN 2018-03-25 16:28:30  29479  收藏 15

分类专栏: 移动端/客户端音视频入门

版权

本教程感谢慕课网

常用工具:

                ffmpeg音视频编解码 

                ffplay音视频播放工具

搭建流媒体服务器

                准备流媒体服务器linux

                准备并安装Nginx服务

                配置RTMP并启动Nginx服务

1.linxu安装Nginx和rtmp 

1.先下载安装  nginx 和 nginx-rtmp 编译依赖工具

sudo apt-get install build-essential libpcre3 libpcre3-dev libssl-dev

2. 创建一个工作目录,并切换到工作目录

mkdir ~/working

cd ~/working

3. 下载 nginx 和 nginx-rtmp源码

wget http://nginx.org/download/nginx-1.7.5.tar.gz

wget https://github.com/arut/nginx-rtmp-module/archive/master.zip

4. 安装unzip工具,解压下载的安装包

sudo apt-get install unzip

5.解压 nginx 和 nginx-rtmp安装包

tar -zxvf nginx-1.7.5.tar.gz

unzip master.zip

6. 切换到 nginx-目录

cd nginx-1.7.5

7.添加 nginx-rtmp 模板编译到 nginx

./configure –with-http_ssl_module –add-module=../nginx-rtmp-module-master

8.编译安装 

make

sudo make install

9. 安装nginx init 脚本

sudo wget https://raw.github.com/JasonGiedymin/nginx-init-ubuntu/master/nginx -O /etc/init.d/nginx

sudo chmod +x /etc/init.d/nginx

sudo update-rc.d nginx defaults

10. 启动和停止nginx 服务,生成配置文件

sudo service nginx start

sudo service nginx stop

11. 安装 FFmpeg

sudo apt-add-repository ppa:jon-severinsson/ffmpeg

sudo apt-get update

sudo apt-get install ffmpeg

原文PPA不可用,源码安装参见

http://blog.csdn.net/redstarofsleep/article/details/45092145

12. 配置 nginx-rtmp 服务器

打开 /usr/local/nginx/conf/nginx.conf

在末尾添加如下 配置

rtmp {    

    server {    

        listen 1935;  #监听的端口  

        chunk_size 4000;    

        application hls {  #rtmp推流请求路径  

            live on;    

            hls on;    

            hls_path /usr/local/nginx/html/hls;    

            hls_fragment 5s;    

        }    

    }    

}

13. 保存上面配置文件,然后重新启动nginx服务

sudo service nginx restart

14. 如果你使用了防火墙,请允许端口 tcp 1935

    用netstat -an | grep 1935

出现以下信息代表则代表启动nginx/rtmp成功

        netstat -an| grep 1935

        tcp        0      0 0.0.0.0:1935            0.0.0.0:*               LISTEN 

16: 使用 客户端,使用 rtmp协议进行视频实时采集

ffmpeg直播命令:

        推流:ffmpeg -re -i 1.mp4 -c copy -f flv rtmp://xxx.xxx.xxx.xxx:1935/hls/1.mp4

        拉流:ffmpeg -i  rtmp://xxx.xxx.xxx.xxx/1.mp4 -c copy dump.flv

        播放音视频:ffplay rtmp://xxx.xxx.xxx.xxx/hls

17.下载一个OBS(视频直播客户端)

(1)配置url

    (2)推流

    (3)服务器播放

                    ffplay rtmp://xxx.xxx.xxx.xxx/hls

18: 关于 nginx-rtmp 更多配置

https://github.com/arut/nginx-rtmp-module/wiki/Directives

flashplay播放器地址

http://bbs.chinaffmpeg.com/1.swf

————————————————

版权声明:本文为CSDN博主「IOT_SHUN」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/IOT_SHUN/java/article/details/79684856

解决使用Nginx错误 Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING问题

Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING问题

 

先说解决办法:直接删除Nginx缓存文件即可;

 

问题描述:

使用Nginx代理的服务,一直使用正常,突然昨天就访问不了了;通过IP访问和端口能正常访问。

 

原本以为是请求头文件过大导致资源未加载完问题;然后修改了Tomcat中配置中的请求头文件,

 

在Tomcat的conf中把server.xml中增加maxHttpHeaderSize的字段或者是把maxHttpHeaderSize的数值调大,如下:

 

 

 

修改后依然无效,浏览器中还是报如下错误:

 

 

 

以上问题原因:

当代理文件大小超过配置的proxy_temp_file_write_size值时,nginx会将文件写入到临时目录下(默认为/proxy_temp)。

 

如果nginx中/proxy_temp过大或者没有权限,就写不进去。

 

解决一:

直接删除Nginx缓存文件,应该就可以访问了:# rm -rf  /usr/local/nginx/proxy_temp

 

防止缓存文件过多,设置Nginx的缓存过期时间,如下:# vim /usr/local/nginx/conf/nginx.conf

 

server

{

listen 80;

server_name mjj.jybb.me; #主机名

location / {

proxy_cache cache_one;

proxy_cache_valid 200 304 3d; #正常状态缓存时间3天,按实际情况修改

proxy_cache_key $host$uri$is_args$args;

proxy_pass http://www.baidu.com/; #反代的网站

proxy_redirect off;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

expires 10d; #默认10天缓存

access_log /home/wwwlogs/mjj.log access; #日志文件,不开启日志请改为access_log off;

}

}

重启一下nginx,访问一下网站

 

解决二:

调整/proxy_temp权限为配置nginx的那个用户。

 

chown -R www:www /usr/local/nginx/proxy_temp

 

安卓编程实现wifi聊天,视频和音频通话

安卓编程实现wifi聊天,视频和音频通话

安卓编程实现wifi聊天,传文件,视频和音频通话
在移动开发的过程中,即时通讯是许多app必须要用到的功能,许多app都没有自主开发自己的聊天系统,反而引用了第三方的即时通讯sdk,贵还不说,有时候想自己增加功能也是麻烦,本文通过安卓编程为例,使用wifi作为服务器,实现了安卓即时通讯和视频音频的通话功能。

安卓开启热点服务
安卓系统自带的热点服务,本次编程将热点开启作为局域网的聊天服务器,一台手机作为主服务器搭建聊天服务。
热点创建服务代码如下
public void createServer(){

try {


// System.out.println(“本机ip:”+getLocalIpAddress());
ServerSocket ss = new ServerSocket(30000);
while (true) {

        String localip= getHostIP();
        System.out.println("本机ip:"+localip);

        Socket s = ss.accept(); 
        System.out.println("已连接ip:"+s.getInetAddress().toString());
        vibrators();


// showNot(“已连接ip:”+s.getInetAddress().toString());
simpleNotice(“已连接ip:”+s.getInetAddress().toString());
socketList.add(s);
new Thread(new ServerThead(s,handler)).start();

        Intent intent = new Intent();
        intent.setAction("www.ailiaw.com.mywifi");//设置意图
        intent.putExtra("action", "newmsg");//设置所需发送的消息标签以及内容


// BroadCastActivity.this.sendBroadcast(intent);
SimpleServer.this.sendOrderedBroadcast(intent,null);//有序广播
}
} catch (IOException e) {
e.printStackTrace();
}
}

客户端也是由安卓手机来接受的,使用普通的Socket连接即可
public void createConnection() {
try {
socket = new Socket(ip, port);
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
if (socket != null) {
try {
socket.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
if (socket != null) {
try {
socket.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
} finally {
}
}

Socket客户端发送文本消息方法
public void sendMessage(String sendMessage) {
try {
out = new DataOutputStream(socket.getOutputStream());
if (sendMessage.equals(“Windows”)) {
out.writeByte(0x1);
out.flush();
return;
}
if (sendMessage.equals(“Unix”)) {
out.writeByte(0x2);
out.flush();
return;
}
if (sendMessage.equals(“Linux”)) {
out.writeByte(0x3);
out.flush();
} else {
out.writeUTF(sendMessage);
out.flush();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
if (out != null) {
try {
out.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}

视频聊天原理:安卓编程获取SurfaceView的视频流转成图片流发送,另一端则接收实时展示图片,形成了动画视频
//获取摄像头流并开启线程通过socket发送
private void initCamera() {
if (!isPreview) {

    if (null == camera) {
        // 打开前置摄像头
        int cameraCount = 0;
        @SuppressWarnings("unused")
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        cameraCount = Camera.getNumberOfCameras(); // get cameras number
        for (int camIdx = 0; camIdx < cameraCount; camIdx++) {
            Camera.getCameraInfo(camIdx, cameraInfo); // get camerainfo
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                //代表摄像头的方位,目前有定义值两个分别为CAMERA_FACING_FRONT前置和CAMERA_FACING_BACK后置
                try {
                    camera = Camera.open(camIdx);
                } catch (RuntimeException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }
    }
    //前置摄像头为空则切换后摄像头
    if (null == camera) {
        // 打开前后置摄像头
        int cameraCount = 0;
        @SuppressWarnings("unused")
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        cameraCount = Camera.getNumberOfCameras(); // get cameras number
        for (int camIdx = 0; camIdx < cameraCount; camIdx++) {
            Camera.getCameraInfo(camIdx, cameraInfo); // get camerainfo
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                //代表摄像头的方位,目前有定义值两个分别为CAMERA_FACING_FRONT前置和CAMERA_FACING_BACK后置
                try {
                    camera = Camera.open(camIdx);
                } catch (RuntimeException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }
    }


// camera = Camera.open();
ClientThread.size = camera.getParameters().getPreviewSize();
}
if (camera != null && !isPreview) {
try {
camera.setPreviewDisplay(sfh); // 通过SurfaceView显示取景画面
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewSize(screenWidth, screenHeight / 4 * 3);
/* 每秒从摄像头捕获5帧画面, */
parameters.setPreviewFrameRate(5);
parameters.setPictureFormat(ImageFormat.NV21); // 设置图片格式
parameters.setPictureSize(screenWidth, screenHeight / 4 * 3); // 设置照片的大小
camera.setDisplayOrientation(90);
camera.setPreviewCallback(new PreviewCallback() {

                @Override
                public void onPreviewFrame(byte[] data, Camera c) {
                    // TODO Auto-generated method stub
                    Size size = camera.getParameters().getPreviewSize();
                    try {
                        //调用image.compressToJpeg()将YUV格式图像数据data转为jpg格式
                        YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
                        if (image != null) {
                            Message msg = clientThread.revHandler.obtainMessage();
                            msg.what = 0x111;
                            msg.obj = image;
                            clientThread.revHandler.sendMessage(msg);

                            /*outstream = new ByteArrayOutputStream();
                            image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, outstream);
                            outstream.flush();
                            new Thread(clientThread).start();*/
                        }
                    } catch (Exception ex) {
                        Log.e("Sys", "Error:" + ex.getMessage());
                    }
                }

            });
            camera.startPreview();                                   // 开始预览
            camera.autoFocus(null);                                  // 自动对焦
        } catch (Exception e) {
            e.printStackTrace();
        }
        isPreview = true;
    }
}


//接收视频流转为图片展示
byte [] buffer = new byte[1024];
int len = 0;

try {
    ss = new ServerSocket(40000);
} catch (IOException e2) {
    // TODO Auto-generated catch block
    e2.printStackTrace();
    return;
}

InputStream ins = null;
while(true){

    try {
        s = ss.accept();
        Log.e("strat","ljq");
        ins = s.getInputStream();

        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        while( (len=ins.read(buffer)) != -1){
            outStream.write(buffer, 0, len);
        }
        ins.close();
        byte data[] = outStream.toByteArray();
        bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);

        Message msg =handler.obtainMessage();
        msg.what = COMPLETED;
        msg.obj = bitmap;
        handler.sendMessage(msg);

        outStream.flush();
        outStream.close();
        if(!s.isClosed()){
            s.close();
        }

    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        return;
    }
    //Bitmap bitmap = BitmapFactory.decodeStream(ins);

}


安卓语音聊天则是通过AudioRecord获取音频流,通过Socket发送,另一端则接收音频流,再通过AudioTrack实时播放出来
.//采集声音并发送
class RecordPlayThread extends Thread{
@SuppressLint(“WrongConstant”)
public void run(){
try{

//byte 文件来存储声音
byte[] buffer = new byte[recBufSize];

//开始采集声音
audioRecord.startRecording();

//播放声音
// audioTrack.play();

        int frame_size = 320;//g726_32 : 4:1的压缩比

        short [] encodeData = new short[frame_size/2];

        while(isRecording){



           /* int bufferReadResult  = audioRecord.read(encodeData, 0, 160);


// calc1(encodeData,0,160);
test_Noise(encodeData,160);

            int wirteNum = audioTrack.write(encodeData, 0, bufferReadResult);*/

            //从MIC存储到缓存区
           int bufferReadResult = audioRecord.read(buffer,0, recBufSize);
            byte[] tmpBuf = new byte[bufferReadResult];





            System.arraycopy(buffer, 0, tmpBuf, 0, bufferReadResult);

            Message msg = clientThread.revHandler.obtainMessage();
            msg.what = 0x111;
            msg.obj = tmpBuf;
            clientThread.revHandler.sendMessage(msg);

            //播放缓存区的数据


// audioTrack.write(tmpBuf, 0, tmpBuf.length);

        }

1
// audioTrack.stop();
// audioRecord.stop();
audioRecord.stop();
audioRecord.release();
audioRecord = null;
// audioTrack.stop();
// audioTrack.release();
// audioTrack = null;
}catch(Throwable t){
Toast.makeText(RecordActivity.this, t.getMessage(),1000);
}
}
};

}

//接收音频声音播放
byte [] buffer = new byte[1024];
int len = 0;

try {
    ss = new ServerSocket(40050);
} catch (IOException e2) {
    // TODO Auto-generated catch block
    e2.printStackTrace();
    return;
}

InputStream ins = null;
while(true){

    try {
        s = ss.accept();
        Log.e("strat","ljq");
        ins = s.getInputStream();

        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        while( (len=ins.read(buffer)) != -1){
            outStream.write(buffer, 0, len);
        }
        ins.close();
        byte[] data = outStream.toByteArray();

        Message msg =handler.obtainMessage();
        msg.what = COMPLETED;
        msg.obj = data;
        handler.sendMessage(msg);

        outStream.flush();
        outStream.close();
        if(!s.isClosed()){
            s.close();
        }

    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        return;
    }

}


class RMyHandler extends Handler {
@Override
public void handleMessage(Message msg){
if (msg.what == COMPLETED) {
try{

            byte[] tmpBuf  = (byte[])msg.obj;

            if(!isplay)
            {
                audioTrack.play();
                isplay=true;
            }


            //播放缓存区的数据
            audioTrack.write(tmpBuf, 0, tmpBuf.length);
            if(!isplay)
            {
            audioTrack.stop();
            audioTrack.release();
            }
         //   audioTrack = null;
            super.handleMessage(msg);

        }catch (Exception e)
        {
            Toast.makeText(RecordActivity.this,"语音播放失败", Toast.LENGTH_SHORT).show();
            e.printStackTrace();
        }

    }
}

}

视频聊天可以演化成直播室形式,只不过只有一个人发送视频流和音频流,其他人则只是接收

第一次写技术博客,希望对大家有用,要完整源码也可以私聊小编
项目地址
https://download.csdn.net/download/zhongzhizhuo/12570465

http://m.ailiaw.top/#/
http://news.ailiaw.top/
————————————————
版权声明:本文为CSDN博主「zhongzhizhuo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhongzhizhuo/article/details/107081393

安卓实现websocket连接,聊天室开发

最近在开发网页小游戏,需要使用到php的Swoole\WebSocket\Server,开发了游戏服务器功能,突然突发奇想,这能不能用来开发移动的即时通讯功能呢?

php服务器的关键代码如下

<?php
//php在线直播示例代码
//使用PHPCLI模式运行
//命令:php start.php

//设置路径
define(‘ROOT‘, dirname(FILE));
require_once ROOT.’/function.php’;
//监听地址和端口
// $server = new swoole_websocket_server(“0.0.0.0(这里就是四个0,不要改)”, 8888);
$server = new swoole_websocket_server(“0.0.0.0”, 8888);
//服务端接收连接事件
$server->on(‘open’, function (swoole_websocket_server $server, $request) {
    if(!file_exists(ROOT.’/client/’.$request->fd.’.client’)){
        @file_put_contents(ROOT.’/chat/’.$request->fd.’.client’,$request->fd);
    }
});
    //服务端接收信息事件
    $server->on(‘message’, function (swoole_websocket_server $server, $frame) {
        foreach(notice(ROOT.’/chat/’) as $v){
            $server->push($v,$frame->data);
        }
    });
        //服务端接收关闭事件
        $server->on(‘close’, function ($ser, $fd) {
            @unlink(ROOT.’/chat/’.$fd.’.client’);
        });
            //服务开启
            $server->start();

服务器的关键代码很简单,也就是做的分发功能而已,接下来就是安卓连接服务器的websocket,接收并发送信息了

public class ClientSocket{

private final int FRAME_QUEUE_SIZE = 5;
private final int CONNECT_TIMEOUT = 120 * 1000;
private WebSocket clientSocket;
private EventBus bus = null;
private String socketUrl;
private WebSocketAdapter adapter = new WebSocketAdapter() {
    @Override
    public void onTextMessage(WebSocket websocket, String text) throws Exception {
        super.onTextMessage(websocket, text);
        String[] msgs = text.split("\\|");
        if (msgs.length >= 2) {
            for (int i = 0; i < msgs.length; i++) {

// L.d(“收到消息:” + msgs[i]);
bus.post(“收到消息:” + msgs[i]);
}
} else {
// L.d(“收到消息:” + text);
bus.post(“收到消息:” + text);
}
}

    @Override
    public void onConnected(WebSocket websocket, Map<String, List<String>> headers)
            throws Exception {
        super.onConnected(websocket, headers);
        bus.post("连接成功");

// L.d(“连接成功”);
}

    @Override
    public void onConnectError(WebSocket websocket, WebSocketException exception)
            throws Exception {
        super.onConnectError(websocket, exception);
        bus.post("连接错误:" + exception.getMessage());

// L.e(“连接错误:” + exception.getMessage());
}

    @Override
    public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer)
            throws Exception {
        super.onDisconnected(websocket, serverCloseFrame, clientCloseFrame, closedByServer);

// L.e(“断开连接”);
bus.post(“断开连接”);
}
};

public ClientSocket(String socketUrl) {
    this.socketUrl = socketUrl;
    bus = EventBus.getDefault();
}

//链接socket
public void connectSocket() {

// L.d(“链接socket”);
if (clientSocket != null && clientSocket.isOpen()) {
clientSocket.sendClose();
clientSocket = null;
}
try {
//ws地址,和设置超时时间
//设置帧队列最大值为5
//设置不允许服务端关闭连接却未发送关闭帧
//添加回调监听
//异步连接
clientSocket = new WebSocketFactory().createSocket(socketUrl, CONNECT_TIMEOUT) //ws地址,和设置超时时间
.setFrameQueueSize(FRAME_QUEUE_SIZE)//设置帧队列最大值为5
.setMissingCloseFrameAllowed(false)//设置不允许服务端关闭连接却未发送关闭帧
.addListener(adapter)//添加回调监听
.connectAsynchronously();
} catch (IOException e) {
e.printStackTrace();
}
}

//发送消息
public void sendMsg(String msg) {
    if (clientSocket != null) {
        if (clientSocket.isOpen()) {//判断已连接
            clientSocket.sendText(msg);
        } else {

// L.d(“clientSocket已经关闭”);
}
} else {
connectSocket();
sendMsg(msg);
// L.e(“clientSocket == null”);
}
}

//离线
public void turnOff() {
    if (clientSocket != null) {
        if (clientSocket.isOpen()) {//判断已连接
            clientSocket.sendClose();//下线
            clientSocket = null;
        }
    } else {

// L.e(“clientSocket == null”);
}
}

}
使用方法

ClientSocketManager clientSocketManager = new ClientSocketManager(“ws://你的服务器ip:8888”);
clientSocketManager.connectSocket(); //连接服务器
clientSocketManager.sendMsg(msg);//发送消息
————————————————
版权声明:本文为CSDN博主「zhongzhizhuo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhongzhizhuo/article/details/107099824

数据库分库分表思路

一. 数据切分

关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。

数据库分布式核心内容无非就是数据切分(Sharding),以及切分后对数据的定位、整合。数据切分就是将数据分散存储到多个数据库中,使得单一数据库中的数据量变小,通过扩充主机的数量缓解单一数据库的性能问题,从而达到提升数据库操作性能的目的。

数据切分根据其切分类型,可以分为两种方式:垂直(纵向)切分和水平(横向)切分

1、垂直(纵向)切分

垂直切分常见有垂直分库和垂直分表两种。

垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与”微服务治理”的做法相似,每个微服务使用单独的一个数据库。如图:

垂直分表是基于数据库中的”列”进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。在字段很多的情况下(例如一个大表有100多个字段),通过”大表拆小表”,更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。

垂直切分的优点:

  • 解决业务系统层面的耦合,业务清晰
  • 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
  • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈

缺点:

  • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
  • 分布式事务处理复杂
  • 依然存在单表数据量过大的问题(需要水平切分)

2、水平(横向)切分

当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。

水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。如图所示: 

库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。

水平切分的优点:

  • 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力
  • 应用端改造较小,不需要拆分业务模块

缺点:

  • 跨分片的事务一致性难以保证
  • 跨库的join关联查询性能较差
  • 数据多次扩展难度和维护量极大

水平切分后同一张表会出现在多个数据库/表中,每个库/表的内容不同。几种典型的数据分片规则为:

1、根据数值范围

按照时间区间或ID区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。某种意义上,某些系统中使用的”冷热数据分离”,将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。

这样的优点在于:

  • 单表大小可控
  • 天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移
  • 使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。

缺点:

  • 热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询

2、根据数值取模

一般采用hash取模mod的切分方式,例如:将 Customer 表根据 cusno 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据会分散到同一个库中,如果查询条件带有cusno字段,则可明确定位到相应库去查询。

优点:

  • 数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈

缺点:

  • 后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题)
  • 容易面临跨分片查询的复杂问题。比如上例中,如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。

二. 分库分表带来的问题

分库分表能有效的环节单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。下面将描述这些技术挑战以及对应的解决思路。 

1、事务一致性问题

分布式事务

当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用”XA协议”和”两阶段提交”处理。

分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。

最终一致性

对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。

2、跨节点关联查询 join 问题

切分之前,系统中很多列表和详情页所需的数据可以通过sql join来完成。而切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用join查询。

解决这个问题的一些方法:

1)全局表

全局表,也可看做是”数据字典表”,就是系统中所有模块都可能依赖的一些表,为了避免跨库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少会进行修改,所以也不担心一致性的问题。

2)字段冗余

一种典型的反范式设计,利用空间换时间,为了性能而避免join查询。例如:订单表保存userId时候,也将userName冗余保存一份,这样查询订单详情时就不需要再去查询”买家user表”了。

但这种方法适用场景也有限,比较适用于依赖字段比较少的情况。而冗余字段的数据一致性也较难保证,就像上面订单表的例子,买家修改了userName后,是否需要在历史订单中同步更新呢?这也要结合实际业务场景进行考虑。

3)数据组装

在系统层面,分两次查询,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。

4)ER分片

关系型数据库中,如果可以先确定表之间的关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能较好的避免跨分片join问题。在1:1或1:n的情况下,通常按照主表的ID主键切分。如下图所示:

这样一来,Data Node1上面的order订单表与orderdetail订单详情表就可以通过orderId进行局部的关联查询了,Data Node2上也一样。

3、跨节点分页、排序、函数问题

跨节点多库进行查询时,会出现limit分页、order by排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。如图所示:

上图中只是取第一页的数据,对性能影响还不是很大。但是如果取得页数很大,情况则变得复杂很多,因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体的排序,这样的操作时很耗费CPU和内存资源的,所以页数越大,系统的性能也会越差。

在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。如图所示:

4、全局主键避重问题

在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。有一些常见的主键生成策略:

1)UUID

UUID标准形式包含32个16进制数字,分为5段,形式为8-4-4-4-12的36个字符,例如:550e8400-e29b-41d4-a716-446655440000

UUID是主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB下,UUID的无序性会引起数据位置频繁变动,导致分页。

2)结合数据库维护主键ID表

在数据库中建立 sequence 表:

CREATE TABLE `sequence` (  
  `id` bigint(20) unsigned NOT NULL auto_increment,  
  `stub` char(1) NOT NULL default '',  
  PRIMARY KEY  (`id`),  
  UNIQUE KEY `stub` (`stub`)  
) ENGINE=MyISAM;

stub字段设置为唯一索引,同一stub值在sequence表中只有一条记录,可以同时为多张表生成全局ID。sequence表的内容,如下所示:

+-------------------+------+  
| id                | stub |  
+-------------------+------+  
| 72157623227190423 |    a |  
+-------------------+------+  

使用 MyISAM 存储引擎而不是 InnoDB,以获取更高的性能。MyISAM使用的是表级别的锁,对表的读写是串行的,所以不用担心在并发时两次读取同一个ID值。

当需要全局唯一的64位ID时,执行:

REPLACE INTO sequence (stub) VALUES ('a');  
SELECT LAST_INSERT_ID();  

这两条语句是Connection级别的,select last_insert_id() 必须与 replace into 在同一数据库连接下才能得到刚刚插入的新ID。

使用replace into代替insert into好处是避免了表行数过大,不需要另外定期清理。

此方案较为简单,但缺点也明显:存在单点问题,强依赖DB,当DB异常时,整个系统都不可用。配置主从可以增加可用性,但当主库挂了,主从切换时,数据一致性在特殊情况下难以保证。另外性能瓶颈限制在单台MySQL的读写性能。

flickr团队使用的一种主键生成策略,与上面的sequence表方案类似,但更好的解决了单点和性能瓶颈的问题。

这一方案的整体思想是:建立2个以上的全局ID生成的服务器,每个服务器上只部署一个数据库,每个库有一张sequence表用于记录当前全局ID。表中ID增长的步长是库的数量,起始值依次错开,这样能将ID的生成散列到各个数据库上。如下图所示:

由两个数据库服务器生成ID,设置不同的auto_increment值。第一台sequence的起始值为1,每次步长增长2,另一台的sequence起始值为2,每次步长增长也是2。结果第一台生成的ID都是奇数(1, 3, 5, 7 …),第二台生成的ID都是偶数(2, 4, 6, 8 …)。

这种方案将生成ID的压力均匀分布在两台机器上。同时提供了系统容错,第一台出现了错误,可以自动切换到第二台机器上获取ID。但有以下几个缺点:系统添加机器,水平扩展时较复杂;每次获取ID都要读写一次DB,DB的压力还是很大,只能靠堆机器来提升性能。

可以基于flickr的方案继续优化,使用批量的方式降低数据库的写压力,每次获取一段区间的ID号段,用完之后再去数据库获取,可以大大减轻数据库的压力。如下图所示:

还是使用两台DB保证可用性,数据库中只存储当前的最大ID。ID生成服务每次批量拉取6个ID,先将max_id修改为5,当应用访问ID生成服务时,就不需要访问数据库,从号段缓存中依次派发0~5的ID。当这些ID发完后,再将max_id修改为11,下次就能派发6~11的ID。于是,数据库的压力降低为原来的1/6。

3)Snowflake分布式自增ID算法

Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位的Long型数字,组成部分:

  • 第一位未使用
  • 接下来41位是毫秒级时间,41位的长度可以表示69年的时间
  • 5位datacenterId,5位workerId。10位的长度最多支持部署1024个节点
  • 最后12位是毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序列

这样的好处是:毫秒数在高位,生成的ID整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高,理论上QPS约为409.6w/s(1000*2^12),并且整个分布式系统内不会产生ID碰撞;可根据自身业务灵活分配bit位。

不足就在于:强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复。

综上

结合数据库和snowflake的唯一ID方案,可以参考业界较为成熟的解法:Leaf——美团点评分布式ID生成系统,并考虑到了高可用、容灾、分布式下时钟等问题。

5、数据迁移、扩容问题

当业务高速发展,面临性能和存储的瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据,然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS,以及业务发展的速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片上的单表数据量不超过1000W)

如果采用数值范围分片,只需要添加节点就可以进行扩容了,不需要对分片数据迁移。如果采用的是数值取模分片,则考虑后期的扩容问题就相对比较麻烦。

三. 什么时候考虑切分

下面讲述一下什么时候需要考虑做数据切分。

1、能不切分尽量不要切分

并不是所有表都需要进行切分,主要还是看数据的增长速度。切分后会在某种程度上提升业务的复杂度,数据库除了承载数据的存储和查询外,协助业务更好的实现需求也是其重要工作之一。

不到万不得已不用轻易使用分库分表这个大招,避免”过度设计”和”过早优化”。分库分表之前,不要为分而分,先尽力去做力所能及的事情,例如:升级硬件、升级网络、读写分离、索引优化等等。当数据量达到单表的瓶颈时候,再考虑分库分表。

2、数据量过大,正常运维影响业务访问

这里说的运维,指:

1)对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO。例如1T的数据,网络传输占50MB时候,需要20000秒才能传输完毕,整个过程的风险都是比较高的

2)对一个很大的表进行DDL修改时,MySQL会锁住全表,这个时间会很长,这段时间业务不能访问此表,影响很大。如果使用pt-online-schema-change,使用过程中会创建触发器和影子表,也需要很长的时间。在此操作过程中,都算为风险时间。将数据表拆分,总量减少,有助于降低这个风险。

3)大表会经常访问与更新,就更有可能出现锁等待。将数据切分,用空间换时间,变相降低访问压力

3、随着业务发展,需要对某些字段垂直拆分

举个例子,假如项目一开始设计的用户表如下:

id                   bigint             #用户的ID
name                 varchar            #用户的名字
last_login_time      datetime           #最近登录时间
personal_info        text               #私人信息
.....                                   #其他信息字段

在项目初始阶段,这种设计是满足简单的业务需求的,也方便快速迭代开发。而当业务快速发展时,用户量从10w激增到10亿,用户非常的活跃,每次登录会更新 last_login_name 字段,使得 user 表被不断update,压力很大。而其他字段:id, name, personal_info 是不变的或很少更新的,此时在业务角度,就要将 last_login_time 拆分出去,新建一个 user_time 表。

personal_info 属性是更新和查询频率较低的,并且text字段占据了太多的空间。这时候,就要对此垂直拆分出 user_ext 表了。

4、数据量快速增长

随着业务的快速发展,单表中的数据量会持续增长,当性能接近瓶颈时,就需要考虑水平切分,做分库分表了。此时一定要选择合适的切分规则,提前预估好数据容量

5、安全性和可用性

鸡蛋不要放在一个篮子里。在业务层面上垂直切分,将不相关的业务的数据库分隔,因为每个业务的数据量、访问量都不同,不能因为一个业务把数据库搞挂而牵连到其他业务。利用水平切分,当一个数据库出现问题时,不会影响到100%的用户,每个库只承担业务的一部分数据,这样整体的可用性就能提高。

四. 案例分析

1、用户中心业务场景

用户中心是一个非常常见的业务,主要提供用户注册、登录、查询/修改等功能,其核心表为:

User(uid, login_name, passwd, sex, age, nickname)

uid为用户ID,  主键
login_name, passwd, sex, age, nickname,  用户属性

任何脱离业务的架构设计都是耍流氓,在进行分库分表前,需要对业务场景需求进行梳理:

  • 用户侧:前台访问,访问量较大,需要保证高可用和高一致性。主要有两类需求:
    • 用户登录:通过login_name/phone/email查询用户信息,1%请求属于这种类型
    • 用户信息查询:登录之后,通过uid来查询用户信息,99%请求属这种类型
  • 运营侧:后台访问,支持运营需求,按照年龄、性别、登陆时间、注册时间等进行分页的查询。是内部系统,访问量较低,对可用性、一致性的要求不高。

2、水平切分方法

当数据量越来越大时,需要对数据库进行水平切分,上文描述的切分方法有”根据数值范围”和”根据数值取模”。

“根据数值范围”:以主键uid为划分依据,按uid的范围将数据水平切分到多个数据库上。例如:user-db1存储uid范围为0~1000w的数据,user-db2存储uid范围为1000w~2000wuid数据。

  • 优点是:扩容简单,如果容量不够,只要增加新db即可。
  • 不足是:请求量不均匀,一般新注册的用户活跃度会比较高,所以新的user-db2会比user-db1负载高,导致服务器利用率不平衡

“根据数值取模”:也是以主键uid为划分依据,按uid取模的值将数据水平切分到多个数据库上。例如:user-db1存储uid取模得1的数据,user-db2存储uid取模得0的uid数据。

  • 优点是:数据量和请求量分布均均匀
  • 不足是:扩容麻烦,当容量不够时,新增加db,需要rehash。需要考虑对数据进行平滑的迁移。

3、非uid的查询方法

水平切分后,对于按uid查询的需求能很好的满足,可以直接路由到具体数据库。而按非uid的查询,例如login_name,就不知道具体该访问哪个库了,此时需要遍历所有库,性能会降低很多。

对于用户侧,可以采用”建立非uid属性到uid的映射关系”的方案;对于运营侧,可以采用”前台与后台分离”的方案。

3.1、建立非uid属性到uid的映射关系

1)映射关系

例如:login_name不能直接定位到数据库,可以建立login_name→uid的映射关系,用索引表或缓存来存储。当访问login_name时,先通过映射表查询出login_name对应的uid,再通过uid定位到具体的库。

映射表只有两列,可以承载很多数据,当数据量过大时,也可以对映射表再做水平切分。这类kv格式的索引结构,可以很好的使用cache来优化查询性能,而且映射关系不会频繁变更,缓存命中率会很高。

2)基因法

分库基因:假如通过uid分库,分为8个库,采用uid%8的方式进行路由,此时是由uid的最后3bit来决定这行User数据具体落到哪个库上,那么这3bit可以看为分库基因。

上面的映射关系的方法需要额外存储映射表,按非uid字段查询时,还需要多一次数据库或cache的访问。如果想要消除多余的存储和查询,可以通过f函数取login_name的基因作为uid的分库基因。生成uid时,参考上文所述的分布式唯一ID生成方案,再加上最后3位bit值=f(login_name)。当查询login_name时,只需计算f(login_name)%8的值,就可以定位到具体的库。不过这样需要提前做好容量规划,预估未来几年的数据量需要分多少库,要预留一定bit的分库基因。

3.2、前台与后台分离

对于用户侧,主要需求是以单行查询为主,需要建立login_name/phone/email到uid的映射关系,可以解决这些字段的查询问题。

而对于运营侧,很多批量分页且条件多样的查询,这类查询计算量大,返回数据量大,对数据库的性能消耗较高。此时,如果和用户侧公用同一批服务或数据库,可能因为后台的少量请求,占用大量数据库资源,而导致用户侧访问性能降低或超时。

这类业务最好采用”前台与后台分离”的方案,运营侧后台业务抽取独立的service和db,解决和前台业务系统的耦合。由于运营侧对可用性、一致性的要求不高,可以不访问实时库,而是通过binlog异步同步数据到运营库进行访问。在数据量很大的情况下,还可以使用ES搜索引擎或Hive来满足后台复杂的查询方式。