Commit e6a55d33 authored by luxq's avatar luxq

implement payment

parent fc2e062f
module payment
go 1.18
require (
github.com/gomodule/redigo v1.8.9
github.com/google/uuid v1.5.0
github.com/redis/go-redis/v9 v9.4.0
github.com/stretchr/testify v1.7.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package payment
import (
"context"
"github.com/redis/go-redis/v9"
"time"
)
type Payment struct {
rdb *redis.Client
}
func NewPayment(redisConfig RedisConnParam) *Payment {
return &Payment{
rdb: redis.NewClient(&redis.Options{
Addr: redisConfig.Addr,
Password: redisConfig.Password,
DB: redisConfig.DbIndex,
}),
}
}
func (p *Payment) IncrBalance(ctx context.Context, uid string, bal int64) (int64, error) {
key := "balance:" + uid
for {
locked, release, err := tryAcquire(ctx, p.rdb, key, 5*time.Second)
if err != nil {
return 0, err
}
if !locked {
continue
}
defer release()
return p.rdb.IncrBy(ctx, key, bal).Result()
}
}
func (p *Payment) GetBalance(ctx context.Context, uid string) (int64, error) {
return p.rdb.Get(ctx, "balance:"+uid).Int64()
}
func (p *Payment) DecrBalance(ctx context.Context, uid string, bal int64) (int64, error) {
key := "balance:" + uid
for {
locked, release, err := tryAcquire(ctx, p.rdb, key, 5*time.Second)
if err != nil {
return 0, err
}
if !locked {
continue
}
defer release()
return p.rdb.DecrBy(ctx, key, bal).Result()
}
}
// also implement three method with key charge
func (p *Payment) IncrCharge(ctx context.Context, uid string, bal int64) (int64, error) {
key := "charge:" + uid
for {
locked, release, err := tryAcquire(ctx, p.rdb, key, 5*time.Second)
if err != nil {
return 0, err
}
if !locked {
continue
}
defer release()
return p.rdb.IncrBy(ctx, key, bal).Result()
}
}
// implement GetCharge
func (p *Payment) GetCharge(ctx context.Context, uid string) (int64, error) {
return p.rdb.Get(ctx, "charge:"+uid).Int64()
}
// implement DecrCharge
func (p *Payment) DecrCharge(ctx context.Context, uid string, bal int64) (int64, error) {
key := "charge:" + uid
for {
locked, release, err := tryAcquire(ctx, p.rdb, key, 5*time.Second)
if err != nil {
return 0, err
}
if !locked {
continue
}
defer release()
return p.rdb.DecrBy(ctx, key, bal).Result()
}
}
// implement IncrCredits
func (p *Payment) IncrCredits(ctx context.Context, uid string, bal int64) (int64, error) {
key := "credits:" + uid
for {
locked, release, err := tryAcquire(ctx, p.rdb, key, 5*time.Second)
if err != nil {
return 0, err
}
if !locked {
continue
}
defer release()
return p.rdb.IncrBy(ctx, key, bal).Result()
}
}
// implement GetCredits
func (p *Payment) GetCredits(ctx context.Context, uid string) (int64, error) {
return p.rdb.Get(ctx, "credits:"+uid).Int64()
}
// implement DecrCredits
func (p *Payment) DecrCredits(ctx context.Context, uid string, bal int64) (int64, error) {
key := "credits:" + uid
for {
locked, release, err := tryAcquire(ctx, p.rdb, key, 5*time.Second)
if err != nil {
return 0, err
}
if !locked {
continue
}
defer release()
return p.rdb.DecrBy(ctx, key, bal).Result()
}
}
func (p *Payment) Close() error {
return p.rdb.Close()
}
package payment
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
)
func TestPayment(t *testing.T) {
// Initialize a new Payment instance
p := NewPayment(RedisConnParam{
Addr: "127.0.0.1:6379",
Password: "123456",
DbIndex: 0,
})
t.Run("Test Balance", func(t *testing.T) {
// Test IncrBalance
_, err := p.IncrBalance(context.Background(), "testuser", 100)
assert.NoError(t, err)
// Test GetBalance
balance, err := p.GetBalance(context.Background(), "testuser")
assert.NoError(t, err)
assert.Equal(t, int64(100), balance)
// Test DecrBalance
_, err = p.DecrBalance(context.Background(), "testuser", 50)
assert.NoError(t, err)
})
t.Run("Test Charge", func(t *testing.T) {
// Test IncrCharge
_, err := p.IncrCharge(context.Background(), "testuser", 200)
assert.NoError(t, err)
// Test GetCharge
charge, err := p.GetCharge(context.Background(), "testuser")
assert.NoError(t, err)
assert.Equal(t, int64(200), charge)
// Test DecrCharge
_, err = p.DecrCharge(context.Background(), "testuser", 100)
assert.NoError(t, err)
})
t.Run("Test Credits", func(t *testing.T) {
// Test IncrCredits
_, err := p.IncrCredits(context.Background(), "testuser", 300)
assert.NoError(t, err)
// Test GetCredits
credits, err := p.GetCredits(context.Background(), "testuser")
assert.NoError(t, err)
assert.Equal(t, int64(300), credits)
// Test DecrCredits
_, err = p.DecrCredits(context.Background(), "testuser", 150)
assert.NoError(t, err)
})
// Close the Payment instance
err := p.Close()
assert.NoError(t, err)
}
package payment
import (
"context"
"fmt"
"github.com/gomodule/redigo/redis"
"github.com/google/uuid"
goredislib "github.com/redis/go-redis/v9"
"strconv"
"strings"
"time"
)
type RedisConnParam struct {
Addr string
Password string
DbIndex int
}
func tryAcquire(ctx context.Context, rs *goredislib.Client, lockKey string, lockTimeout time.Duration) (acquired bool, release func(), _ error) {
timeout := time.Now().Add(lockTimeout).UnixNano()
lockToken := fmt.Sprintf("%d,%s", timeout, uuid.New().String())
release = func() {
// Best effort to check we're releasing the lock we think we have. Note that it
// is still technically possible the lock token has changed between the GET and
// DEL since these are two separate operations, i.e. when the current lock happen
// to be expired at this very moment.
get, _ := rs.Get(ctx, lockKey).Result()
if get == lockToken {
_ = rs.Del(ctx, lockKey)
}
}
set, err := rs.SetNX(ctx, lockKey, lockToken, lockTimeout).Result()
if err != nil {
return false, nil, err
} else if set {
return true, release, nil
}
// We didn't get the lock, but we can check if the lock is expired.
currentLockToken, err := rs.Get(ctx, lockKey).Result()
if err == redis.ErrNil {
// Someone else got the lock and released it already.
return false, nil, nil
} else if err != nil {
return false, nil, err
}
currentTimeout, _ := strconv.ParseInt(strings.SplitN(currentLockToken, ",", 2)[0], 10, 64)
if currentTimeout > time.Now().UnixNano() {
// The lock is still valid.
return false, nil, nil
}
// The lock has expired, try to acquire it.
get, err := rs.GetSet(ctx, lockKey, lockToken).Result()
if err != nil {
return false, nil, err
} else if get != currentLockToken {
// Someone else got the lock
return false, nil, nil
}
// We got the lock.
return true, release, nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment