Quick.Data.Redis.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. { ***************************************************************************
  2. Copyright (c) 2015-2020 Kike Pérez
  3. Unit : Quick.Data.Redis
  4. Description : Redis client
  5. Author : Kike Pérez
  6. Version : 1.0
  7. Created : 22/02/2020
  8. Modified : 12/07/2020
  9. This file is part of QuickLib: https://github.com/exilon/QuickLib
  10. ***************************************************************************
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. *************************************************************************** }
  21. unit Quick.Data.Redis;
  22. {$i QuickLib.inc}
  23. interface
  24. uses
  25. System.SysUtils,
  26. System.DateUtils,
  27. IdTCPClient,
  28. Quick.Commons;
  29. type
  30. IRedisResponse = interface
  31. ['{21EF7ABF-E678-4F18-AE56-8A7C6B817AE3}']
  32. function GetIsDone: Boolean;
  33. function GetResponse: string;
  34. procedure SetIsDone(const Value: Boolean);
  35. procedure SetResponse(const Value: string);
  36. property IsDone : Boolean read GetIsDone write SetIsDone;
  37. property Response : string read GetResponse write SetResponse;
  38. end;
  39. TRedisResponse = class(TInterfacedObject,IRedisResponse)
  40. private
  41. fIsDone : Boolean;
  42. fResponse : string;
  43. function GetIsDone: Boolean;
  44. function GetResponse: string;
  45. procedure SetIsDone(const Value: Boolean);
  46. procedure SetResponse(const Value: string);
  47. public
  48. property IsDone : Boolean read GetIsDone write SetIsDone;
  49. property Response : string read GetResponse write SetResponse;
  50. end;
  51. TRedisSortedItem = record
  52. Value : string;
  53. Score : Int64;
  54. end;
  55. TRedisClient = class
  56. private
  57. fTCPClient : TIdTCPClient;
  58. fHost : string;
  59. fPort : Integer;
  60. fDataBaseNumber : Integer;
  61. fMaxSize : Int64;
  62. fPassword : string;
  63. fConnectionTimeout : Integer;
  64. fReadTimeout : Integer;
  65. fConnected : Boolean;
  66. procedure SetConnectionTimeout(const Value: Integer);
  67. procedure SetReadTimeout(const Value: Integer);
  68. function Command(const aCommand : string; const aArguments : string = '') : IRedisResponse; overload;
  69. function Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse; overload;
  70. function EscapeString(const json: string) : string;
  71. function IsIntegerResult(const aValue : string) : Boolean;
  72. public
  73. constructor Create;
  74. destructor Destroy; override;
  75. property Host : string read fHost write fHost;
  76. property Port : Integer read fPort write fPort;
  77. property DataBaseNumber : Integer read fDataBaseNumber write fDataBaseNumber;
  78. property MaxSize : Int64 read fMaxSize write fMaxSize;
  79. property Password : string read fPassword write fPassword;
  80. property ConnectionTimeout : Integer read fConnectionTimeout write SetConnectionTimeout;
  81. property ReadTimeout : Integer read fReadTimeout write SetReadTimeout;
  82. property Connected : Boolean read fConnected;
  83. function RedisSELECT(dbIndex : Integer) : Boolean;
  84. function RedisSET(const aKey, aValue : string; aTTLMs : Integer = -1) : Boolean;
  85. function RedisGET(const aKey : string; out oValue : string) : Boolean;
  86. function RedisDEL(const aKey : string) : Boolean;
  87. function RedisRPUSH(const aKey, aValue : string) : Boolean;
  88. function RedisLPUSH(const aKey, aValue : string) : Boolean;
  89. function RedisRPOP(const aKey : string; out oValue : string) : Boolean;
  90. function RedisBRPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  91. function RedisLPOP(const aKey : string; out oValue : string) : Boolean;
  92. function RedisBLPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  93. function RedisBRPOPLPUSH(const aKey, aKeyToMove: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  94. function RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  95. function RedisEXPIRE(const aKey : string; aTTLMs : Integer) : Boolean; overload;
  96. function RedisEXPIRE(const aKey : string; aExpireDate : TDateTime) : Boolean; overload;
  97. function RedisLINDEX(const aKey: string; aIndex: Integer; out oValue : string): Boolean;
  98. function RedisLREM(const aKey, aValue: string; aNumOccurrences: Integer): Boolean;
  99. function RedisZADD(const aKey, aValue : string; aScore : Int64) : Boolean;
  100. function RedisZREM(const aKey, aValue : string) : Boolean;
  101. function RedisZRANGE(const aKey : string; aStartPosition, aEndPosition : Int64) : TArray<string>;
  102. function RedisZRANGEBYSCORE(const aKey : string; aMinScore, aMaxScore : Int64) : TArray<TRedisSortedItem>;
  103. function RedisLLEN(const aKey : string): Integer;
  104. function RedisTTL(const aKey, aValue : string): Integer;
  105. function RedisAUTH(const aPassword : string) : Boolean;
  106. function RedisPING : Boolean;
  107. function RedisQUIT : Boolean;
  108. procedure Connect;
  109. procedure Disconnect;
  110. end;
  111. ERedisConnectionError = class(Exception);
  112. ERedisAuthError = class(Exception);
  113. ERedisCommandError = class(Exception);
  114. implementation
  115. const
  116. DEF_REDIS_PORT = 6379;
  117. CRLF = #10#13;
  118. DEF_CONNECTIONTIMEOUT = 30000;
  119. DEF_READTIMETOUT = 10000;
  120. { TRedisResponse }
  121. function TRedisResponse.GetIsDone: Boolean;
  122. begin
  123. Result := fIsDone;
  124. end;
  125. function TRedisResponse.GetResponse: string;
  126. begin
  127. Result := fResponse;
  128. end;
  129. procedure TRedisResponse.SetIsDone(const Value: Boolean);
  130. begin
  131. fIsDone := Value;
  132. end;
  133. procedure TRedisResponse.SetResponse(const Value: string);
  134. begin
  135. fResponse := Value;
  136. end;
  137. { TRedisClient }
  138. constructor TRedisClient.Create;
  139. begin
  140. inherited;
  141. fConnected := False;
  142. fHost := 'localhost';
  143. fPort := DEF_REDIS_PORT;
  144. fDataBaseNumber := 0;
  145. fMaxSize := 0;
  146. fPassword := '';
  147. fConnectionTimeout := DEF_CONNECTIONTIMEOUT;
  148. fReadTimeout := DEF_READTIMETOUT;
  149. fTCPClient := TIdTCPClient.Create;
  150. end;
  151. destructor TRedisClient.Destroy;
  152. begin
  153. try
  154. if fTCPClient.Connected then RedisQUIT;
  155. fTCPClient.IOHandler.InputBuffer.Clear;
  156. fTCPClient.IOHandler.WriteBufferFlush;
  157. if fTCPClient.Connected then fTCPClient.Disconnect(False);
  158. fTCPClient.Free;
  159. except
  160. //avoid closing errors
  161. end;
  162. inherited;
  163. end;
  164. procedure TRedisClient.Disconnect;
  165. begin
  166. if fConnected then RedisQUIT;
  167. fConnected := False;
  168. end;
  169. procedure TRedisClient.Connect;
  170. begin
  171. if not fTCPClient.Connected then
  172. begin
  173. fTCPClient.Host := fHost;
  174. fTCPClient.Port := fPort;
  175. fTCPClient.ConnectTimeout := fConnectionTimeout;
  176. fTCPClient.ReadTimeout := fConnectionTimeout;
  177. end;
  178. try
  179. fTCPClient.Connect; //first connection
  180. //connect password and database
  181. if not fTCPClient.Connected then
  182. begin
  183. fTCPClient.Connect;
  184. if not fTCPClient.Connected then raise ERedisConnectionError.Create('Can''t connect to Redis Server!');
  185. end;
  186. fTCPClient.Socket.Binding.SetKeepAliveValues(True,5000,1000);
  187. if fPassword <> '' then
  188. begin
  189. if not RedisAUTH(fPassword) then raise ERedisAuthError.Create('Redis authentication error!');
  190. end;
  191. if fDataBaseNumber > 0 then
  192. begin
  193. if not RedisSELECT(fDataBaseNumber) then raise ERedisConnectionError.CreateFmt('Can''t select Redis Database "%d"',[fDataBaseNumber]);
  194. end;
  195. fConnected := True;
  196. except
  197. on E : Exception do raise ERedisConnectionError.CreateFmt('Can''t connect to Redis service %s:%d (%s)',[Self.Host,Self.Port,e.Message]);
  198. end;
  199. end;
  200. function TRedisClient.IsIntegerResult(const aValue: string): Boolean;
  201. begin
  202. Result := IsInteger(StringReplace(aValue,':','',[]));
  203. end;
  204. function TRedisClient.EscapeString(const json: string): string;
  205. begin
  206. Result := StringReplace(json,'\','\\',[rfReplaceAll]);
  207. Result := StringReplace(Result,'"','\"',[rfReplaceAll]);
  208. //Result := StringReplace(Result,'/','\/"',[rfReplaceAll]);
  209. end;
  210. procedure TRedisClient.SetConnectionTimeout(const Value: Integer);
  211. begin
  212. if fConnectionTimeout <> Value then
  213. begin
  214. fConnectionTimeout := Value;
  215. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fConnectionTimeout;
  216. end;
  217. end;
  218. procedure TRedisClient.SetReadTimeout(const Value: Integer);
  219. begin
  220. if fReadTimeout <> Value then
  221. begin
  222. fReadTimeout := Value;
  223. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fReadTimeout;
  224. end;
  225. end;
  226. function TRedisClient.Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse;
  227. begin
  228. Result := Command(aCommand,Format(aArgumentsFormat,aValues));
  229. end;
  230. function TRedisclient.Command(const aCommand : string; const aArguments : string = '') : IRedisResponse;
  231. function TrimResponse(const aResponse : string) : string;
  232. begin
  233. Result := Copy(aResponse,Low(aResponse) + 1, aResponse.Length);
  234. end;
  235. var
  236. res : string;
  237. begin
  238. Result := TRedisResponse.Create;
  239. try
  240. if not fTCPClient.Connected then Connect;
  241. fTCPClient.IOHandler.Write(aCommand + ' ' + aArguments + CRLF);
  242. if fTCPClient.IOHandler.CheckForDataOnSource(fReadTimeout) then
  243. begin
  244. res := fTCPClient.IOHandler.ReadLn;
  245. if not res.IsEmpty then
  246. case res[Low(res)] of
  247. '+' :
  248. begin
  249. if res.Contains('+OK') then
  250. begin
  251. Result.IsDone := True;
  252. end
  253. else Result.Response := TrimResponse(res);
  254. end;
  255. '-' : Result.Response := TrimResponse(res);
  256. ':' :
  257. begin
  258. Result.Response := TrimResponse(res);
  259. Result.IsDone := Result.Response.ToInteger > -1;
  260. end;
  261. '$' :
  262. begin
  263. Result.Response := TrimResponse(res);
  264. if IsInteger(Result.Response) then
  265. begin
  266. if Result.Response.ToInteger > -1 then Result.IsDone := True;
  267. end
  268. else Result.IsDone := True;
  269. end;
  270. '*' :
  271. begin
  272. Result.Response := TrimResponse(res);
  273. Result.IsDone := True;
  274. end;
  275. end;
  276. end;
  277. except
  278. on E : Exception do raise ERedisCommandError.CreateFmt('%s error: %s',[aCommand,e.message]);
  279. end;
  280. end;
  281. function TRedisClient.RedisRPUSH(const aKey, aValue : string) : Boolean;
  282. begin
  283. Result := Command('RPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  284. end;
  285. function TRedisClient.RedisSELECT(dbIndex: Integer): Boolean;
  286. begin
  287. Result := Command('SELECT',dbIndex.ToString).IsDone;
  288. end;
  289. function TRedisClient.RedisSET(const aKey, aValue: string; aTTLMs: Integer = -1): Boolean;
  290. begin
  291. Result := Command('SET','%s "%s" PX %d',[aKey,EscapeString(aValue),aTTLMs]).IsDone;
  292. end;
  293. function TRedisClient.RedisRPOP(const aKey: string; out oValue: string): Boolean;
  294. begin
  295. Result := False;
  296. if Command('RPOP','%s',[aKey]).IsDone then
  297. begin
  298. oValue := fTCPClient.IOHandler.ReadLn;
  299. Result := True;
  300. end;
  301. end;
  302. function TRedisClient.RedisBRPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  303. var
  304. response : IRedisResponse;
  305. begin
  306. Result := False;
  307. response := Command('BRPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  308. if response.IsDone then
  309. begin
  310. fTCPClient.IOHandler.ReadLn; //$int
  311. fTCPClient.IOHandler.ReadLn; //key
  312. fTCPClient.IOHandler.ReadLn; //$int
  313. oValue := fTCPClient.IOHandler.ReadLn; //value
  314. Result := True;
  315. end
  316. else raise ERedisCommandError.CreateFmt('BLPOP Error: %s',[response.Response]);
  317. end;
  318. function TRedisClient.RedisBRPOPLPUSH(const aKey, aKeyToMove: string; out oValue: string; aWaitTimeoutSecs: Integer): Boolean;
  319. var
  320. response : IRedisResponse;
  321. begin
  322. Result := False;
  323. response := Command('BRPOPLPUSH','%s %s %d',[aKey,aKeyToMove,aWaitTimeoutSecs]);
  324. if response.IsDone then
  325. begin
  326. oValue := fTCPClient.IOHandler.ReadLn; //value
  327. Result := True;
  328. end
  329. else raise ERedisCommandError.CreateFmt('BRPOPLPUSH Error: %s',[response.Response]);
  330. end;
  331. function TRedisClient.RedisDEL(const aKey: string): Boolean;
  332. begin
  333. Result := Command('DEL',aKey).IsDone;
  334. end;
  335. function TRedisClient.RedisLLEN(const aKey : string): Integer;
  336. var
  337. response : IRedisResponse;
  338. begin
  339. Result := 0;
  340. response := Command('LLEN',aKey);
  341. if response.IsDone then
  342. begin
  343. Result := response.Response.ToInteger;
  344. end;
  345. end;
  346. function TRedisClient.RedisTTL(const aKey, aValue : string): Integer;
  347. var
  348. response : IRedisResponse;
  349. begin
  350. Result := 0;
  351. response := Command('TTL','%s "%s"',[aKey,EscapeString(aValue)]);
  352. if response.IsDone then
  353. begin
  354. Result := response.Response.ToInteger;
  355. end;
  356. end;
  357. function TRedisClient.RedisZADD(const aKey, aValue: string; aScore: Int64): Boolean;
  358. var
  359. response : IRedisResponse;
  360. begin
  361. response := Command('ZADD','%s %d "%s"',[aKey,aScore,EscapeString(aValue)]);
  362. if response.IsDone then
  363. begin
  364. Result := response.Response.ToInteger = 1;
  365. end
  366. else raise ERedisCommandError.CreateFmt('ZADD %s',[response.Response]);
  367. end;
  368. function TRedisClient.RedisZRANGE(const aKey: string; aStartPosition, aEndPosition: Int64): TArray<string>;
  369. var
  370. response : IRedisResponse;
  371. item : TRedisSortedItem;
  372. value : string;
  373. score : string;
  374. i : Integer;
  375. begin
  376. Result := [];
  377. response := Command('ZRANGE','%s %d %d',[aKey,aStartPosition,aEndPosition]);
  378. if response.IsDone then
  379. begin
  380. for i := 1 to (response.Response.ToInteger) do
  381. begin
  382. fTCPClient.IOHandler.ReadLn; //$int
  383. value := fTCPClient.IOHandler.ReadLn; //value
  384. Result := Result + [value];
  385. end;
  386. end
  387. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  388. end;
  389. function TRedisClient.RedisZRANGEBYSCORE(const aKey: string; aMinScore, aMaxScore: Int64): TArray<TRedisSortedItem>;
  390. var
  391. response : IRedisResponse;
  392. item : TRedisSortedItem;
  393. i : Integer;
  394. value : string;
  395. score : string;
  396. begin
  397. Result := [];
  398. response := Command('ZRANGEBYSCORE','%s %d %d WITHSCORES',[aKey,aMinScore,aMaxScore]);
  399. if response.IsDone then
  400. begin
  401. for i := 1 to (response.Response.ToInteger Div 2) do
  402. begin
  403. fTCPClient.IOHandler.ReadLn; //$int
  404. value := fTCPClient.IOHandler.ReadLn; //value
  405. fTCPClient.IOHandler.ReadLn; //$int
  406. score := fTCPClient.IOHandler.ReadLn; //score
  407. item.Value := value;
  408. item.Score := score.ToInt64;
  409. Result := Result + [item];
  410. end;
  411. end
  412. else raise ERedisCommandError.CreateFmt('ZRANGE Error: %s',[response.Response]);
  413. end;
  414. function TRedisClient.RedisZREM(const aKey, aValue: string): Boolean;
  415. var
  416. response : IRedisResponse;
  417. begin
  418. response := Command('ZREM','%s "%s"',[aKey,EscapeString(aValue)]);
  419. if response.IsDone then
  420. begin
  421. Result := response.Response.ToInteger = 1;
  422. end;
  423. end;
  424. function TRedisClient.RedisLPOP(const aKey: string; out oValue: string): Boolean;
  425. begin
  426. Result := False;
  427. if Command('LPOP','%s',[aKey]).IsDone then
  428. begin
  429. oValue := fTCPClient.IOHandler.ReadLn;
  430. Result := True;
  431. end;
  432. end;
  433. function TRedisClient.RedisBLPOP(const aKey: string; out oValue: string; aWaitTimeoutSecs : Integer): Boolean;
  434. var
  435. response : IRedisResponse;
  436. begin
  437. Result := False;
  438. response := Command('BLPOP','%s %d',[aKey,aWaitTimeoutSecs]);
  439. if response.IsDone then
  440. begin
  441. fTCPClient.IOHandler.ReadLn; //$int
  442. fTCPClient.IOHandler.ReadLn; //key
  443. fTCPClient.IOHandler.ReadLn; //$int
  444. oValue := fTCPClient.IOHandler.ReadLn; //value
  445. Result := True;
  446. end
  447. else raise ERedisCommandError.CreateFmt('BLPOP Error: %s',[response.Response]);
  448. end;
  449. function TRedisClient.RedisLPUSH(const aKey, aValue : string) : Boolean;
  450. begin
  451. Result := Command('LPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  452. end;
  453. function TRedisClient.RedisLREM(const aKey, aValue: string; aNumOccurrences: Integer): Boolean;
  454. begin
  455. Result := Command('LREM','%s "%s" %d',[aKey,EscapeString(aValue),aNumOccurrences * -1]).IsDone;
  456. end;
  457. function TRedisClient.RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  458. begin
  459. Result := Command('LTRIM','%s %d %d',[aKey,aFirstElement,fMaxSize]).IsDone;
  460. end;
  461. function TRedisClient.RedisAUTH(const aPassword : string) : Boolean;
  462. begin
  463. Result := Command('AUTH',fPassword).IsDone;
  464. end;
  465. function TRedisClient.RedisEXPIRE(const aKey: string; aExpireDate: TDateTime): Boolean;
  466. begin
  467. Result := RedisEXPIRE(aKey,MilliSecondsBetween(Now(),aExpireDate));
  468. end;
  469. function TRedisClient.RedisEXPIRE(const aKey: string; aTTLMs: Integer): Boolean;
  470. begin
  471. Result := Command('PEXPIRE','%s %d',[aKey,aTTLMs]).IsDone;
  472. end;
  473. function TRedisClient.RedisLINDEX(const aKey: string; aIndex: Integer; out oValue : string): Boolean;
  474. var
  475. response : IRedisResponse;
  476. begin
  477. Result := False;
  478. response := Command('LINDEX','%s %d',[aKey,aIndex]);
  479. if response.IsDone then
  480. begin
  481. oValue := response.response;
  482. Result := True;
  483. end;
  484. end;
  485. function TRedisClient.RedisGET(const aKey: string; out oValue: string): Boolean;
  486. begin
  487. Result := False;
  488. if Command('GET','%s',[aKey]).IsDone then
  489. begin
  490. oValue := fTCPClient.IOHandler.ReadLn;
  491. Result := True;
  492. end;
  493. end;
  494. function TRedisClient.RedisPING : Boolean;
  495. begin
  496. Result := False;
  497. if Command('PING').IsDone then
  498. begin
  499. Result := fTCPClient.IOHandler.ReadLn = 'PONG';
  500. end;
  501. end;
  502. function TRedisClient.RedisQUIT : Boolean;
  503. begin
  504. try
  505. Result := Command('QUIT').IsDone;
  506. except
  507. Result := False;
  508. end;
  509. end;
  510. end.