主库:weloe/token-go: a light login library.
扩展库:weloe/token-go-extensions (github.com)
本篇给主库扩展一个Adapter提供简单的外部数据存储。
一个库/框架往往不能完成所有事情,需要其他库/框架的支持才能达到更加完善的效果。本篇会对token-go框架的Adapter进行简单的拓展。
首先我们应该想想Adapter是用来干什么的?
从第一篇我们就明确其职责,就是存储数据。我们在token-go里提供了一个内置的adapter:default_adapter,用于框架底层的数据存储,但是这种内存的数据存储有着很多的缺陷,并且没有经过实际的生产测试使用。也因此,我们应该提供更成熟的存储方案来提供给使用者去替代它。
这就是本篇要实现的redis_adapter了
这里还有一个点要注意,将数据存储到外部需要确定数据的序列化和反序列化方法。因此,我们加了一个SerializerAdapter接口,要求新的Adapter选择实现。
token-go/serializer_adapter.go at master · weloe/token-go · GitHub
package persist
import "github.com/weloe/token-go/model"
type SerializerAdapter interface {
Adapter
Serialize(*model.Session) ([]byte, error)
UnSerialize([]byte) (*model.Session, error)
}
具体的调用则是在enforcer对session进行存储或者取出数据的时候进行调用。
func (e *Enforcer) GetSession(id string) *model.Session {
if v := e.adapter.Get(e.spliceSessionKey(id)); v != nil {
if s := e.sessionUnSerialize(v); s != nil {
return s
} else {
session, ok := v.(*model.Session)
if !ok {
return nil
}
return session
}
}
return nil
}
这里的sessionUnSerialize()
实际上就是尝试调用了adapter实现的反序列化方法。同理SetSession()
也是一样的。
最后就是RedisAdapter了
token-go-extensions/adapter.go at master · weloe/token-go-extensions · GitHub
并不难,只要实现我们之前的Adapter和SerializerAdapter两个接口就行了。
序列化方法使用json,方便查看
package redis_adapter
import (
"context"
"encoding/json"
"github.com/go-redis/redis/v8"
"github.com/weloe/token-go/model"
"github.com/weloe/token-go/persist"
"time"
)
var _ persist.Adapter = (*RedisAdapter)(nil)
var _ persist.SerializerAdapter = (*RedisAdapter)(nil)
type RedisAdapter struct {
client *redis.Client
}
func (r *RedisAdapter) Serialize(session *model.Session) ([]byte, error) {
return json.Marshal(session)
}
func (r *RedisAdapter) UnSerialize(bytes []byte) (*model.Session, error) {
s := &model.Session{}
err := json.Unmarshal(bytes, s)
if err != nil {
return nil, err
}
return s, nil
}
func (r *RedisAdapter) GetStr(key string) string {
res, err := r.client.Get(context.Background(), key).Result()
if err != nil {
return ""
}
return res
}
func (r *RedisAdapter) SetStr(key string, value string, timeout int64) error {
err := r.client.Set(context.Background(), key, value, time.Duration(timeout)*time.Second).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) UpdateStr(key string, value string) error {
err := r.client.Set(context.Background(), key, value, 0).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) DeleteStr(key string) error {
err := r.client.Del(context.Background(), key).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) GetStrTimeout(key string) int64 {
duration, err := r.client.TTL(context.Background(), key).Result()
if err != nil {
return -1
}
return int64(duration.Seconds())
}
func (r *RedisAdapter) UpdateStrTimeout(key string, timeout int64) error {
var duration time.Duration
if timeout < 0 {
duration = -1
} else {
duration = time.Duration(timeout) * time.Second
}
err := r.client.Expire(context.Background(), key, duration).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) Get(key string) interface{} {
res, err := r.client.Get(context.Background(), key).Result()
if err != nil {
return nil
}
s := &model.Session{}
err = json.Unmarshal([]byte(res), s)
if err != nil {
return nil
}
return s
}
func (r *RedisAdapter) Set(key string, value interface{}, timeout int64) error {
err := r.client.Set(context.Background(), key, value, time.Duration(timeout)*time.Second).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) Update(key string, value interface{}) error {
err := r.client.Set(context.Background(), key, value, 0).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) Delete(key string) error {
err := r.client.Del(context.Background(), key).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) GetTimeout(key string) int64 {
duration, err := r.client.TTL(context.Background(), key).Result()
if err != nil {
return -1
}
return int64(duration.Seconds())
}
func (r *RedisAdapter) UpdateTimeout(key string, timeout int64) error {
var duration time.Duration
if timeout < 0 {
duration = -1
} else {
duration = time.Duration(timeout) * time.Second
}
err := r.client.Expire(context.Background(), key, duration).Err()
if err != nil {
return err
}
return nil
}
func (r *RedisAdapter) DeleteBatchFilteredKey(filterKeyPrefix string) error {
var cursor uint64 = 0
for {
keys, cursor, err := r.client.Scan(context.Background(), cursor, filterKeyPrefix+"*", 100).Result()
if err != nil {
return err
}
if len(keys) == 0 && cursor == 0 {
break
}
// use pip delete batch
pipe := r.client.Pipeline()
for _, key := range keys {
pipe.Del(context.Background(), key)
}
_, err = pipe.Exec(context.Background())
if err != nil {
return err
}
}
return nil
}
实现完接口,再写几个初始化方法
func NewAdapter(addr string, username string, password string, db int) (*RedisAdapter, error) {
return NewAdapterByOptions(&redis.Options{
Addr: addr,
Username: username,
Password: password,
DB: db,
})
}
func NewAdapterByOptions(options *redis.Options) (*RedisAdapter, error) {
client := redis.NewClient(options)
_, err := client.Ping(context.Background()).Result()
if err != nil {
return nil, err
}
return &RedisAdapter{client: client}, nil
}
就不贴测试代码了,就放个链接~
token-go-extensions/adapter_test.go at master · weloe/token-go-extensions · GitHub
这样RedisAdapter就开发完了吗?不不不,并没有。
用户量的增大,对容错,一致性等等的要求提高,可能需要用到多个redis,这就需要我们继续适配开发一个ClusterAdapter了,为什么我这里不往下写了?阳了好累当然是因为还在开发中~~
0.前言其实先学Vue、elementUI,还是先学jQuery,纠结过一阵子。 毕竟,很多人都说jQuery过时了。 jQuery能做到的,Vue都可以做到。但是,Spring全家桶提供了非常好的开发生态,如果不是非常大的大型项目,个人感觉SpringBoot+thymeleaf+jQuery已经够用,这也是最快速的全栈方案——不涉及过多的前端专用框架。而layUI,就是专门面向后端程序员开发前端的(一站式方案)。但是,想要实现前后端分离,还是得用vue(或类似框架),所以,后面再学。 1.入手官网下载,都是中文没啥困难。 官方文档已经非常简单详细,这里介绍一下官方文档的使用方法,以及必备套路。文档:里面都是开发文档的详细介绍,从头开始看就行。示例:这里就是我们copy代码的宝库,里面几乎将所有的使用方法都提供了演示和代码,方便按需copy。还能在线调试周边:其中的“扩展组件”提供了基于layUI的组件,有时候会用到,比如dtree2.开始使用创建springboot项目,引入springweb和thymeleaf。将下载包中的layui文件夹整个放入resources/static
1.背景在SRS使用中实现视频录制功能。2.思路方案1:实时方式拍照:操作者点击拍照按钮,触发网络请求,后端收到请求后启动一个ffmpeg命令行进行截图。录像:操作者点击开始录像按钮,触发网络请求,后端收到请求后启动一个ffmpeg命令行录像,待操作者点击结束录像按钮后录像完成。方案优点:容易实现方案缺点:操作和响应的延时,即点击按钮后,约有2-5秒延迟(网络响应时间+ffmpeg启动时间+ffmpeg打开流时间+ffmpeg拍照响应时间。 争议:看到的视频的当前播放内容(时间)!=点击按钮时间!=ffmpeg拍照时间方案2:DVR录制后拍照和截取前提:开启DVR功能,对每一个来自客户端发布的流都录制,并以时间分段成多个物理文件,然后就可以操作文件截图和截取视频了。拍照:点击按钮获得点击时间,从已经完成的DVR文件中识别文件名(包含了时间),读取文件后按指定时间差值截图。录像:获得开始录制时间和结束录制时间。从已完成的DVR文件中识别文件名(包含了时间),读取文件截取时间段内的截图。方案优点:相比较于方案1,时间误差的延迟小。方案缺点:依赖于DVR录制后的文件。需要考虑DVR临时录制文
函数的语法有两种格式可以用来在shell脚本中创建函数,第一种格式采用关键字function,后跟分配给该代码块的函数名 name属性定义了赋予函数的唯一名称,脚本中定义的每个函数都必须有一个唯一的名称functionname{ commands } 或者 name(){ commands }复制实现函数[root@linux/]#vimxxx.sh #!/bin/bash functionwww{ echo"Helloworld" } www [root@linux/]#shxxx.sh Helloworld复制调用函数[root@linux/]#vimxxx.sh #!/bin/bash functionwe(){ echo"Helloworld" } www=1 while[$www-le3] do we#调用函数 www=$[$www+1] done [root@linux/]#shxxx.sh Helloworld Helloworld Helloworld复制函数传参函数传参调用语法: 函数名参数1参数2....[root@lin
上期内容:Vivado下不可不知的快捷键很多情况下需要对原有列表进行修改,这种修改通常包括:获取指定范围内的元素形成子列表;插入新的元素形成新列表;删除列表中的元素;替换列表中的元素;修改指定索引的列表元素等,对此,Tcl都提供了相应的命令。lrange功能:获取指定范围内的元素形成子列表lrange需要三个参数:列表、第一个索引值和第二个索引值。索引值可以包含end,且第二个索引值大于第一个索引值,如下图所示。如果第二个索引值小于第一个索引值,则返回空列表。这在程序调试时非常有帮助,如果发现列表为空,需要检查一下是否索引值的顺序颠倒。linsert功能:插入新的元素形成新的列表linsert需要至少三个参数。其中第一个参数是原始列表,第二个参数是新元素在新列表中的索引,第三个及后续参数为插入值。如下图所示,索引为0,表明新插入的元素位于新列表的0号位置;若为1,则在1号位置;若为end则在末位。同时可以看到,linsert返回一个新的列表,并不会改变原始列表,所以puts$type的输出保持不变。当参数多于3个时,第三个参数到最后一个参数均被视为插入值。同时,若索引值大于列表最大索引
#!/bin/bash set-e command1 command2 ... exit0 ---------------------------------------------------------- Everyscriptyouwriteshouldincludeset-eatthetop.Thistellsbashthatitshouldexitthescriptifanystatementreturnsanon-truereturnvalue.Thebenefitofusing-eisthatitpreventserrorssnowballingintoseriousissueswhentheycouldhavebeencaughtearlier.Again,forreadabilityyoumaywanttouseset-oerrexit.复制你写的每个脚本都应该在文件开头加上set-e,这句语句告诉bash如果任何语句的执行结果不是true则应该退出。这样的好处是防止错误像滚雪球般变大导致一个致命的错误,而这些错误本应该在之前就被处理掉。如果要增加可读性,可以
一级标题 启动和内核管理面试题 1、linux系统查看当前加载的模块?查看⼀个模块信息和相关参数的⽅法?加载 ⼀个模块? lsmod命令查看已加载模块 modinfo命令显示kernel模块的信息。 modprobe命令加载某个模块 2、linux系统中开机启动⽂件路径是__/etc/rc.d/__。 3、linux常⽤的引导加载程序是__LILO__和__GRUB__。 4、linux系统中,⼀般可⽤__c__实现⾃动编译。 a.gccb.gdbc.maked.vi 5、简述linux开机启动过程? CentOS6启动流程 1.加载BIOS的硬件信息,获取第一个启动设备 2.读取第一个启动设备MBR的引导加载程序(grub)的启动信息 3.加载核心操作系统的核心信息,核心开始解压缩,并尝试驱动所有的硬件设备 4.核心执行init程序,并获取默认的运行信息 5.init程序执行/etc/rc.d/rc.sysinit文件 6.启动核心的外挂模块 7.init执行运行的各个批处理文件(scripts) 8.init执行/etc/rc.d/rc.local 9.执行/bin/login程序
1、查询所有列 select*from表名; 2、查询表结构 desc表名; 3、查询指定列 selectename,sal,jobfrom表名; 4、racle中查看所有表和字段 获取表: selecttable_namefromuser_tables;//当前用户的表 selecttable_namefromall_tables;//所有用户的表 selecttable_namefromdba_tables;//包括系统表 selecttable_namefromdba_tableswhereowner='用户名' user_tables: table_name,tablespace_name,last_analyzed等 dba_tables: ower,table_name,tablespace_name,last_analyzed等 all_tables: ower,table_name,tablespace_name,last_analyzed等 all_object
问题1:UnityEditor.BuildPlayerWindow+BuildMethodException:Buildfailedwitherrors.这个问题经常出现,困扰蛮久的,最后在论坛找到一个靠谱的解决办法,很简单!就是因为因为打包内容的问题,导致unity不允许apk的路径是Assets文件夹下,所以设置为其他文件夹或者干脆桌面即可解决。 参考网址(英文描述) 问题2:ArgumentException:TheAssemblySystem.ConfigurationisreferencedbySystem.Data('Assets/Plugins/System.Data.dll').Butthedllisnotallowedtobeincludedorcouldnotbefound. 这个问题不仅在Android会出现,在发布exe的时候也会出现,其实解决方法也很简单。 分析:System.Configuration来自System.Data,但是找不到,可以通过修改设置里的.Net解决,降低为.Net2.0即可,如图 如果是发布安卓出现这个问题如图解决,然后发布Wi
其实很简单,直接把界面的控件传入直接打印控件的内容: privatevoidButton_Click(objectsender,RoutedEventArgse) { PrintDialogdialog=newPrintDialog(); if(dialog.ShowDialog()==true) { dialog.PrintVisual(控件名,"PrintTest"); } }复制 上述方法确实能打印了,但是发现不能居中打印,不能打印多页。 以下是网上找到的方法:(https://www.cnblogs.com/naliang/p/wpfprint.html) publicvoidprint(FrameworkElementViewContainer) { FrameworkElementobjectToPrint=ViewContainerasFrameworkElement; PrintDialogprintDialog=newPrintDialog(); printDialog.PrintTicket.PageOrientation=PageOrie
1.javacHelloWorld.java dir显示生成了HelloWorld.class文件; 2.javaHelloWorld 错误:找不到或无法加载主类HelloWorld 3.解决方法:运行时加上包名 javac-d.HelloWorld.java javahomework1.HelloWorld 说明: (1)javac编译的时候需要加-d.; javac-d.HelloWorld.java中 . 前后一定要有空格 javac运行之后,会根据包名生成相对应的文件夹名在HelloWorld文件目录下 (2)java运行的时候,要带上包名; javahomework1.HelloWorld 1MicrosoftWindows[版本10.0.18362.30] 2(c)2019MicrosoftCorporation。保留所有权利。 3 4C:\Users\Lenovo>cd/ 5 6
sklearn依赖于scipy,而scipy依赖于numpy+mkl。 所以想要安装sklearn包,顺序应该为 1.安装numpy+mkl 2.安装scipy 3.安装sklearn 直接使用pip安装这些包有时会出现问题,解决方法是到http://www.lfd.uci.edu/~gohlke/pythonlibs/下载相应的包的.whl文件,再用pip本地安装whl文件。 以上为windows环境的安装方法,很麻烦,如果只是想配置开发环境,建议直接使用Anaconda,它将常用的科学计算库都打包好了,如果想在服务器上部署相关应用,建议弃用windows拥抱*nix,因为实在太麻烦了。在Linux端安装sklearn则很简单,只需要用pip安装numpy/scipty/sklearn即可。(如果使用pip安装太慢的话,可以尝试更换国内镜像源,详细方法请参考我的博文:将pip的下载源更改为国内镜像)
常见表示: Dp[i][j]=符合要求且以数字j为首位的i位数的个数 Dp[i][j]=符合要求且处于状态j的i位数的个数,如j=0/1表示首位不是/是4 步骤:init();//计算所有的dp[][]solve(n);//计算[0,n]的答案[l,r]=solve(r)–solve(l-1); 1longlongdp[10][10]={0}; 2voidinit() 3{ 4dp[0][0]=1; 5for(inti=1;i<=7;i++){ 6for(intj=0;j<=9;j++){//枚举第i位 7for(intk=0;k<=9;k++){//枚举第i-1位 8if(符合要求)continue; 9dp[i][j]+=dp[i-1][k]; 10} 11} 12} 13}复制 1intnum[10]; 2longlongsolve(longlongn) 3{ 4longlongt=0; 5memset(num,0,sizeof(num)); 6while(n){ 7num[++t]=n%10; 8n/=10; 9} 10num[
1、分享一个Unity中用于管理声音的声音管理器,适合于中小型项目。 2、借鉴了很多的源码,最后修改完成,吸取百家之长,改为自己所用。 3、源码如下: 1/* 2* 3*开发时间:2018.11.20 4* 5*功能:用来对项目中的所有音频做同一的管理 6* 7*描述: 8*1、挂载该脚本的游戏物体上要挂载三个AudioSouce[可以修改脚本,动态挂] 9*2、建议大的背景音乐不要加入AudioClip[],对内存消耗大。而是哪里用到,单独使用PlayBackground函数播放 10*3、小的音效片段可以将其加入AudioClip[]中,可以很方便的管理,可以通过声音剪辑、声音剪辑名称来进行音乐的播放 11*4、该声音管理可以进行背景音乐的播放、音效的播放、背景音乐的音调改变、音效的音调改变、停止播放 12* 13*/ 14usingUnityEngine; 15usingSystem.Collections; 16usingSystem.Collections.Generic; 17 18namespaceKernal 19{ 20publicclassAudio
Canvas canvas最早由Apple引入WebKit,用于MacOSX的Dashboard,后来又在Safari和GoogleChrome被实现。基于Gecko1.8的浏览器,比如Firefox1.5,同样支持这个元素。<canvas>元素是WhatWGWebapplications1.0规范的一部分,也包含于HTML5中。 体验Canvas 什么是Canvas? HTML5的canvas元素使用JavaScript在网页上绘制图像。画布是一个矩形区域,您可以控制其每一像素。canvas拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。 创建Canvas元素 向HTML5页面添加canvas元素。规定元素的id、宽度和高度: <canvasid="myCanvas"width="200"height="100"></canvas> Canvas坐标系 通过JavaScript来绘制 /*获取元素*/ varmyCanvas=document.querySelector('#myCanvas'); /*获取绘图工具*/ varcon
一.InnoDB逻辑存储结构 首先要先介绍一下InnoDB逻辑存储结构和区的概念,它的所有数据都被逻辑地存放在表空间,表空间又由段,区,页组成。 段 段就是上图的segment区域,常见的段有数据段、索引段、回滚段等,在InnoDB存储引擎中,对段的管理都是由引擎自身所完成的。 区 区就是上图的extent区域,区是由连续的页组成的空间,无论页的大小怎么变,区的大小默认总是为1MB。为了保证区中的页的连续性,InnoDB存储引擎一次从磁盘申请4-5个区,InnoDB页的大小默认为16kb,即一个区一共有64(1MB/16kb=16)个连续的页。每个段开始,先用32页(page)大小的碎片页来存放数据,在使用完这些页之后才是64个连续页的申请。这样做的目的是,对于一些小表或者是undo类的段,可以开始申请较小的空间,节约磁盘开销。 页 页就是上图的page区域,也可以叫块。页是InnoDB磁盘管理的最小单位。默认大小为16KB,可以通过参数innodb_page_size来设置。常见的页类型有:数据页,undo页,系统页,事务数据页,插入缓冲位图页,插入缓冲空闲列表页,
当调用FragmentTransaction#hide(fragment)和FragmentTransaction#show(fragment)方法,fragment不会调用生命周期方法onResume和onPause() Fragment的onResume和onPause调用时机与Activity相同 @Override publicvoidonHiddenChanged(booleanhidden){ super.onHiddenChanged(hidden); if(hidden){ //getSupportFragmentManager().beginTransaction().hide(fragment); onPause(); }else{ //getSupportFragmentManager().beginTransaction().show(fragment); onResume(); } } 复制
首先,引入子进程模块 var process = require('child_process'); 执行shell命令 调用该模块暴露出来的方法exec process.exec('shutdown-hnow',function(error,stdout,stderr){ if(error!==null){ console.log('execerror:'+error); } });//回调函数非必须!复制 执行.sh脚本 很多时候需要多个命令来完成一项工作,而这个工作又常常是重复的,这个时候我们自然会想到将这些命令写成sh脚本,下次执行下这个脚本一切就都搞定了,下面就是发布代码的一个脚本示例。 编写脚本 touchupdateapp.sh vimupdateapp.sh 复制 #切换目录 cd/home/ubuntu/mobile #更新代码 gitpulloriginmaster #重启apache服务 sudoserviceapache2restart 复制 执行脚本 管理员身份 sudosu yourpasswo
正则表达式一直是困扰很多程序员的一门技术,当然也包括曾经的我。大多数时候我们在开发过程中要用到某些正则表达式的时候,都会打开谷歌或百度直接搜索然后拷贝粘贴。当下一次再遇到相同问题的时候,同样的场景又再来一遍。作为一门用途很广的技术,我相信深入理解正则表达式并能融会贯通是值得的。所以,希望这篇文章能帮助大家理清思路,搞懂正则表达式各种符号之间的内在联系,形成知识体系,当下次再遇到正则表达式的时候可以不借助搜索引擎,自己解决。 正则表达式到底是什么 正则表达式(RegularExpression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于上个20世纪50年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。大家可以利用https://regexper.com这个工具很好地可视化自己写的正则表达式: 如/\d\w+/这个正则生成的状态机图: 对于具体的算法实现,大家如果感兴趣可以阅读《算法导论》。 从字符出发